feat: 增强后台设置校验、代理权限控制与财务审计能力
This commit is contained in:
@@ -78,7 +78,7 @@ final class AuditAgentLineDataCommand extends Command
|
||||
->select('admin_site_id', DB::raw('count(*) as cnt'))
|
||||
->where('depth', '>', 0)
|
||||
->groupBy('admin_site_id')
|
||||
->having('cnt', '>', 0)
|
||||
->havingRaw('count(*) > 0')
|
||||
->get();
|
||||
|
||||
foreach ($sitesWithManyBusinessAgents as $row) {
|
||||
|
||||
178
app/Console/Commands/AuditFinancialChainCommand.php
Normal file
178
app/Console/Commands/AuditFinancialChainCommand.php
Normal file
@@ -0,0 +1,178 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
final class AuditFinancialChainCommand extends Command
|
||||
{
|
||||
protected $signature = 'lottery:audit-financial-chain {--json : Output JSON only}';
|
||||
|
||||
protected $description = '只读审计钱包、转账、授信、结算与收付款资金链闭环';
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
$issues = array_merge(
|
||||
$this->walletIssues(),
|
||||
$this->transferIssues(),
|
||||
$this->creditIssues(),
|
||||
$this->settlementIssues(),
|
||||
$this->paymentIssues(),
|
||||
);
|
||||
|
||||
if ($this->option('json')) {
|
||||
$this->line(json_encode(['issues' => $issues], JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT));
|
||||
|
||||
return $issues === [] ? self::SUCCESS : self::FAILURE;
|
||||
}
|
||||
|
||||
if ($issues === []) {
|
||||
$this->info('Financial chain audit passed.');
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
$this->error(sprintf('Financial chain audit found %d issue(s).', count($issues)));
|
||||
foreach ($issues as $issue) {
|
||||
$this->line(sprintf('- [%s] %s', $issue['type'], $issue['message']));
|
||||
}
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<array{type: string, message: string, count: int}>
|
||||
*/
|
||||
private function walletIssues(): array
|
||||
{
|
||||
return $this->countIssues([
|
||||
'wallet_txn_math_bad' => [
|
||||
'message' => '已入账钱包流水的前后余额不匹配',
|
||||
'sql' => "select count(*) as cnt from wallet_txns where status = 'posted' and ((direction = 1 and balance_after <> balance_before + amount) or (direction = 2 and balance_after <> balance_before - amount) or direction not in (1,2))",
|
||||
],
|
||||
'wallet_negative' => [
|
||||
'message' => '玩家钱包余额或冻结余额为负数',
|
||||
'sql' => 'select count(*) as cnt from player_wallets where balance < 0 or frozen_balance < 0',
|
||||
],
|
||||
'wallet_latest_mismatch' => [
|
||||
'message' => '玩家钱包当前余额与最新已入账流水余额不一致',
|
||||
'sql' => "select count(*) as cnt from player_wallets w join wallet_txns t on t.wallet_id = w.id where t.status = 'posted' and t.id = (select max(t2.id) from wallet_txns t2 where t2.wallet_id = w.id and t2.status = 'posted') and w.balance <> t.balance_after",
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<array{type: string, message: string, count: int}>
|
||||
*/
|
||||
private function transferIssues(): array
|
||||
{
|
||||
return $this->countIssues([
|
||||
'success_transfer_missing_txn' => [
|
||||
'message' => '成功转账单缺少对应钱包流水',
|
||||
'sql' => "select count(*) as cnt from transfer_orders o left join wallet_txns t on t.biz_no = o.transfer_no and t.biz_type = case when o.direction = 'out' then 'transfer_out' else 'transfer_in' end and t.status = 'posted' where o.status = 'success' and t.id is null",
|
||||
],
|
||||
'transfer_txn_missing_order' => [
|
||||
'message' => '转账类钱包流水找不到对应转账单',
|
||||
'sql' => "select count(*) as cnt from wallet_txns t left join transfer_orders o on o.transfer_no = t.biz_no where t.status = 'posted' and t.biz_type in ('transfer_in','transfer_out','transfer_out_refund','reversal') and o.id is null",
|
||||
],
|
||||
'transfer_amount_mismatch' => [
|
||||
'message' => '成功转账单金额与对应钱包流水金额不一致',
|
||||
'sql' => "select count(*) as cnt from transfer_orders o join wallet_txns t on t.biz_no = o.transfer_no and t.status = 'posted' and t.biz_type = case when o.direction = 'out' then 'transfer_out' else 'transfer_in' end where o.status = 'success' and o.amount <> t.amount",
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<array{type: string, message: string, count: int}>
|
||||
*/
|
||||
private function creditIssues(): array
|
||||
{
|
||||
return $this->countIssues([
|
||||
'credit_account_negative' => [
|
||||
'message' => '玩家授信账户额度、已用或冻结为负数',
|
||||
'sql' => 'select count(*) as cnt from player_credit_accounts where credit_limit < 0 or used_credit < 0 or frozen_credit < 0',
|
||||
],
|
||||
'credit_account_over_limit' => [
|
||||
'message' => '玩家已用授信加冻结授信超过授信额度',
|
||||
'sql' => 'select count(*) as cnt from player_credit_accounts where used_credit + frozen_credit > credit_limit',
|
||||
],
|
||||
'credit_players_without_account' => [
|
||||
'message' => '信用盘玩家缺少授信账户',
|
||||
'sql' => "select count(*) as cnt from players p left join player_credit_accounts a on a.player_id = p.id where p.funding_mode = 'credit' and a.player_id is null",
|
||||
],
|
||||
'orphan_player_credit_ledger' => [
|
||||
'message' => '玩家信用流水引用了不存在的玩家',
|
||||
'sql' => "select count(*) as cnt from credit_ledger cl left join players p on p.id = cl.owner_id where cl.owner_type = 'player' and p.id is null",
|
||||
],
|
||||
'orphan_credit_ticket_item_ref' => [
|
||||
'message' => '信用流水引用了不存在的注单明细',
|
||||
'sql' => "select count(*) as cnt from credit_ledger cl where cl.ref_type = 'ticket_item' and not exists (select 1 from ticket_items ti where ti.id = cl.ref_id)",
|
||||
],
|
||||
'orphan_credit_bill_ref' => [
|
||||
'message' => '信用流水引用了不存在的结算账单',
|
||||
'sql' => "select count(*) as cnt from credit_ledger cl where cl.ref_type = 'settlement_bill' and not exists (select 1 from settlement_bills sb where sb.id = cl.ref_id)",
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<array{type: string, message: string, count: int}>
|
||||
*/
|
||||
private function settlementIssues(): array
|
||||
{
|
||||
return $this->countIssues([
|
||||
'settlement_bill_math_bad' => [
|
||||
'message' => '结算账单金额闭环异常(已付 + 未付 + 坏账核销不等于应结绝对值)',
|
||||
'sql' => "select count(*) as cnt from settlement_bills where abs(net_amount) <> paid_amount + unpaid_amount + case when bill_type <> 'bad_debt' then coalesce(cast(meta_json ->> 'written_off_amount' as bigint), 0) else 0 end",
|
||||
],
|
||||
'settlement_bill_negative_paid_unpaid' => [
|
||||
'message' => '结算账单已付或未付金额为负数',
|
||||
'sql' => 'select count(*) as cnt from settlement_bills where paid_amount < 0 or unpaid_amount < 0',
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<array{type: string, message: string, count: int}>
|
||||
*/
|
||||
private function paymentIssues(): array
|
||||
{
|
||||
return $this->countIssues([
|
||||
'payment_amount_nonpositive' => [
|
||||
'message' => '收付款记录金额小于等于 0',
|
||||
'sql' => 'select count(*) as cnt from payment_records where amount <= 0',
|
||||
],
|
||||
'confirmed_payment_missing_time' => [
|
||||
'message' => '已确认收付款记录缺少确认时间',
|
||||
'sql' => "select count(*) as cnt from payment_records where status = 'confirmed' and confirmed_at is null",
|
||||
],
|
||||
'bill_paid_mismatch_confirmed_payments' => [
|
||||
'message' => '账单已付金额与已确认收付款汇总不一致',
|
||||
'sql' => "with p as (select settlement_bill_id, coalesce(sum(amount),0) confirmed_amount from payment_records where status = 'confirmed' group by settlement_bill_id) select count(*) as cnt from settlement_bills b left join p on p.settlement_bill_id = b.id where b.paid_amount <> coalesce(p.confirmed_amount,0)",
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, array{message: string, sql: string}> $checks
|
||||
* @return list<array{type: string, message: string, count: int}>
|
||||
*/
|
||||
private function countIssues(array $checks): array
|
||||
{
|
||||
$issues = [];
|
||||
|
||||
foreach ($checks as $type => $check) {
|
||||
$count = (int) (DB::selectOne($check['sql'])->cnt ?? 0);
|
||||
if ($count > 0) {
|
||||
$issues[] = [
|
||||
'type' => $type,
|
||||
'message' => $check['message'],
|
||||
'count' => $count,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return $issues;
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ use App\Http\Controllers\Controller;
|
||||
use App\Services\Agent\AgentAdminUserService;
|
||||
use App\Lottery\ErrorCode;
|
||||
use App\Support\AdminAgentNodeAccess;
|
||||
use App\Support\AdminAgentScope;
|
||||
use App\Support\AdminUserApiPresenter;
|
||||
use App\Support\ApiMessage;
|
||||
use App\Http\Requests\Admin\AgentAdminUserStoreRequest;
|
||||
@@ -39,6 +40,17 @@ final class AgentNodeAdminUserStoreController extends Controller
|
||||
);
|
||||
}
|
||||
|
||||
if (! AdminAgentScope::nodeManageableBy($admin, $agent_node)) {
|
||||
return AdminAgentNodeAccess::denyUnlessCanManageParent($admin, $agent_node)
|
||||
?? ApiMessage::errorResponse(
|
||||
$request,
|
||||
'admin.agent_user_manage_denied',
|
||||
ErrorCode::AdminForbidden->value,
|
||||
null,
|
||||
403,
|
||||
);
|
||||
}
|
||||
|
||||
$user = $service->createUnderAgent($agent_node, $request->validated());
|
||||
|
||||
AuditLogger::recordForAdmin(
|
||||
|
||||
@@ -47,6 +47,14 @@ final class AgentNodeDestroyController extends Controller
|
||||
return ApiMessage::errorResponse($request, 'admin.agent_node_has_players_cannot_delete', ErrorCode::ValidationFailed->value, null, 422);
|
||||
}
|
||||
|
||||
if (DB::table('admin_user_agents')->where('agent_node_id', $agent_node->id)->exists()) {
|
||||
return ApiMessage::errorResponse($request, 'admin.agent_node_has_admin_users_cannot_delete', ErrorCode::ValidationFailed->value, null, 422);
|
||||
}
|
||||
|
||||
if ($service->hasBlockingCustomRoles($agent_node)) {
|
||||
return ApiMessage::errorResponse($request, 'admin.agent_node_has_roles_cannot_delete', ErrorCode::ValidationFailed->value, null, 422);
|
||||
}
|
||||
|
||||
$before = AgentNodePresenter::item($agent_node);
|
||||
$service->destroy($agent_node);
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ use App\Http\Controllers\Controller;
|
||||
use App\Services\Agent\AgentRoleService;
|
||||
use App\Lottery\ErrorCode;
|
||||
use App\Support\AdminAgentNodeAccess;
|
||||
use App\Support\AdminAgentScope;
|
||||
use App\Support\AdminRoleApiPresenter;
|
||||
use App\Support\ApiMessage;
|
||||
use App\Http\Requests\Admin\AgentRoleStoreRequest;
|
||||
@@ -39,6 +40,17 @@ final class AgentNodeRoleStoreController extends Controller
|
||||
);
|
||||
}
|
||||
|
||||
if (! AdminAgentScope::nodeManageableBy($admin, $agent_node)) {
|
||||
return AdminAgentNodeAccess::denyUnlessCanManageParent($admin, $agent_node)
|
||||
?? ApiMessage::errorResponse(
|
||||
$request,
|
||||
'admin.agent_role_manage_denied',
|
||||
ErrorCode::AdminForbidden->value,
|
||||
null,
|
||||
403,
|
||||
);
|
||||
}
|
||||
|
||||
$role = $service->createForAgent($admin, $agent_node, $request->validated());
|
||||
|
||||
AuditLogger::recordForAdmin(
|
||||
|
||||
@@ -4,6 +4,7 @@ namespace App\Http\Requests\Admin;
|
||||
|
||||
use App\Models\AdminUser;
|
||||
use App\Http\Requests\ApiFormRequest;
|
||||
use App\Support\AdminSettingPolicy;
|
||||
|
||||
final class AdminSettingBatchUpdateRequest extends ApiFormRequest
|
||||
{
|
||||
@@ -22,8 +23,8 @@ final class AdminSettingBatchUpdateRequest extends ApiFormRequest
|
||||
|
||||
foreach ($items as $item) {
|
||||
$key = is_array($item) ? (string) ($item['key'] ?? '') : '';
|
||||
if (str_starts_with($key, 'settlement.')) {
|
||||
return $admin->hasAdminPermission('prd.payout.manage');
|
||||
if (! AdminSettingPolicy::canUpdate($admin, $key)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,4 +39,15 @@ final class AdminSettingBatchUpdateRequest extends ApiFormRequest
|
||||
'items.*.value' => ['present'],
|
||||
];
|
||||
}
|
||||
|
||||
public function after(): array
|
||||
{
|
||||
return [
|
||||
function (): void {
|
||||
/** @var list<array{key: string, value: mixed}> $items */
|
||||
$items = $this->validated('items', []);
|
||||
AdminSettingPolicy::validateItems($items);
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ namespace App\Http\Requests\Admin;
|
||||
|
||||
use App\Models\AdminUser;
|
||||
use App\Http\Requests\ApiFormRequest;
|
||||
use App\Support\AdminSettingPolicy;
|
||||
|
||||
final class AdminSettingUpdateRequest extends ApiFormRequest
|
||||
{
|
||||
@@ -15,11 +16,7 @@ final class AdminSettingUpdateRequest extends ApiFormRequest
|
||||
}
|
||||
|
||||
$key = (string) $this->route('key', '');
|
||||
if (str_starts_with($key, 'settlement.')) {
|
||||
return $admin->hasAdminPermission('prd.payout.manage');
|
||||
}
|
||||
|
||||
return true;
|
||||
return AdminSettingPolicy::canUpdate($admin, $key);
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
@@ -28,4 +25,17 @@ final class AdminSettingUpdateRequest extends ApiFormRequest
|
||||
'value' => ['present'],
|
||||
];
|
||||
}
|
||||
|
||||
public function after(): array
|
||||
{
|
||||
return [
|
||||
function (): void {
|
||||
$key = (string) $this->route('key', '');
|
||||
AdminSettingPolicy::validateItems([[
|
||||
'key' => $key,
|
||||
'value' => $this->validated('value'),
|
||||
]]);
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -443,7 +443,7 @@ final class AdminUser extends Authenticatable
|
||||
return false;
|
||||
}
|
||||
|
||||
return count(array_intersect($needed, $effective)) > 0;
|
||||
return count(array_diff($needed, $effective)) === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -133,7 +133,6 @@ final class AdminDashboardSnapshotBuilder
|
||||
{
|
||||
return $admin->hasPermissionCode('service.reconcile.manage')
|
||||
|| $admin->hasPermissionCode('service.reconcile.view')
|
||||
|| $admin->hasPermissionCode('service.wallet.view')
|
||||
|| $admin->hasPermissionCode('service.wallet.manage');
|
||||
}
|
||||
|
||||
|
||||
@@ -6,13 +6,20 @@ use App\Models\AdminUser;
|
||||
use App\Models\AgentNode;
|
||||
use App\Models\AgentProfile;
|
||||
use App\Models\Player;
|
||||
use App\Support\AdminScopeContext;
|
||||
use App\Support\AdminScopeContextResolver;
|
||||
use App\Support\AdminAgentSettlementScope;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
/** 代理账号仪表盘:授信、团队规模、待结账单摘要。 */
|
||||
final class AgentDashboardOverviewBuilder
|
||||
{
|
||||
public function __construct(
|
||||
private readonly AdminReportQueryService $reportQuery,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>|null
|
||||
*/
|
||||
@@ -46,6 +53,16 @@ final class AgentDashboardOverviewBuilder
|
||||
}
|
||||
|
||||
$pendingBillStats = $this->pendingBillStats($admin, $subtreeIds);
|
||||
$scope = AdminScopeContextResolver::fromValues($admin, requestedAgentNodeId: (int) $node->id);
|
||||
$today = now()->toDateString();
|
||||
$sevenDayFrom = now()->subDays(6)->toDateString();
|
||||
$todayTotals = $this->reportQuery->periodFinanceTotals($today, $today, $scope);
|
||||
$sevenDayTotals = $this->reportQuery->periodFinanceTotals($sevenDayFrom, $today, $scope);
|
||||
$currencyCode = $this->reportQuery->resolvePeriodCurrencyCode($today, $today, $scope)
|
||||
?? $this->reportQuery->resolvePeriodCurrencyCode($sevenDayFrom, $today, $scope);
|
||||
$teamPlayerStats = $this->teamPlayerStats($subtreeIds);
|
||||
$todayActivityStats = $this->todayActivityStats($subtreeIds, $today);
|
||||
$topAgentToday = $this->topAgentToday($scope, $today, (int) $node->id);
|
||||
|
||||
return [
|
||||
'agent_node_id' => (int) $node->id,
|
||||
@@ -61,13 +78,25 @@ final class AgentDashboardOverviewBuilder
|
||||
),
|
||||
'total_share_rate' => (float) ($profile?->total_share_rate ?? 0),
|
||||
'settlement_cycle' => (string) ($profile?->settlement_cycle ?? 'weekly'),
|
||||
'can_create_child_agent' => $profile === null || $profile->can_create_child_agent,
|
||||
'can_create_player' => $profile === null || $profile->can_create_player,
|
||||
'can_create_child_agent' => (bool) ($profile?->can_create_child_agent ?? false),
|
||||
'can_create_player' => (bool) ($profile?->can_create_player ?? false),
|
||||
'direct_child_count' => $directChildCount,
|
||||
'subtree_agent_count' => count($subtreeIds),
|
||||
'direct_player_count' => $directPlayerCount,
|
||||
'team_player_count' => $teamPlayerStats['count'],
|
||||
'active_player_count_today' => $todayActivityStats['player_count'],
|
||||
'bet_order_count_today' => $todayActivityStats['order_count'],
|
||||
'today_bet_minor' => $todayTotals['total_bet_minor'],
|
||||
'today_payout_minor' => $todayTotals['total_payout_minor'],
|
||||
'today_profit_minor' => $todayTotals['approx_house_gross_minor'],
|
||||
'seven_day_bet_minor' => $sevenDayTotals['total_bet_minor'],
|
||||
'seven_day_payout_minor' => $sevenDayTotals['total_payout_minor'],
|
||||
'seven_day_profit_minor' => $sevenDayTotals['approx_house_gross_minor'],
|
||||
'currency_code' => $currencyCode,
|
||||
'pending_bill_count' => $pendingBillStats['count'],
|
||||
'pending_unpaid_minor' => $pendingBillStats['unpaid_minor'],
|
||||
'latest_bet_at' => $todayActivityStats['latest_bet_at'],
|
||||
'top_agent_today' => $topAgentToday,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -94,4 +123,64 @@ final class AgentDashboardOverviewBuilder
|
||||
'unpaid_minor' => (int) $query->sum('unpaid_amount'),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<int> $subtreeIds
|
||||
* @return array{count: int}
|
||||
*/
|
||||
private function teamPlayerStats(array $subtreeIds): array
|
||||
{
|
||||
if ($subtreeIds === [] || ! Schema::hasColumn('players', 'agent_node_id')) {
|
||||
return ['count' => 0];
|
||||
}
|
||||
|
||||
return [
|
||||
'count' => (int) Player::query()
|
||||
->whereIn('agent_node_id', $subtreeIds)
|
||||
->count(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<int> $subtreeIds
|
||||
* @return array{player_count: int, order_count: int, latest_bet_at: ?string}
|
||||
*/
|
||||
private function todayActivityStats(array $subtreeIds, string $today): array
|
||||
{
|
||||
if ($subtreeIds === []) {
|
||||
return [
|
||||
'player_count' => 0,
|
||||
'order_count' => 0,
|
||||
'latest_bet_at' => null,
|
||||
];
|
||||
}
|
||||
|
||||
$base = DB::table('ticket_orders as o')
|
||||
->join('players as p', 'p.id', '=', 'o.player_id')
|
||||
->whereIn('p.agent_node_id', $subtreeIds)
|
||||
->whereDate('o.created_at', $today);
|
||||
|
||||
$latestBetAt = (clone $base)->max('o.created_at');
|
||||
|
||||
return [
|
||||
'player_count' => (int) (clone $base)->distinct('o.player_id')->count('o.player_id'),
|
||||
'order_count' => (int) (clone $base)->count(),
|
||||
'latest_bet_at' => $latestBetAt !== null ? Carbon::parse((string) $latestBetAt)->toIso8601String() : null,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>|null
|
||||
*/
|
||||
private function topAgentToday(AdminScopeContext $scope, string $today, int $currentAgentId): ?array
|
||||
{
|
||||
$rows = $this->reportQuery->agentRankingRows($today, $today, null, 5, $scope);
|
||||
foreach ($rows as $row) {
|
||||
if ((int) ($row['agent_node_id'] ?? 0) === $currentAgentId) {
|
||||
return $row;
|
||||
}
|
||||
}
|
||||
|
||||
return $rows[0] ?? null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -274,14 +274,14 @@ final class AgentProfileService
|
||||
{
|
||||
$profile = $this->profileForNode($agentNodeId);
|
||||
|
||||
return $profile === null || $profile->can_create_child_agent;
|
||||
return (bool) ($profile?->can_create_child_agent ?? false);
|
||||
}
|
||||
|
||||
public function nodeMayCreatePlayer(int $agentNodeId): bool
|
||||
{
|
||||
$profile = $this->profileForNode($agentNodeId);
|
||||
|
||||
return $profile === null || $profile->can_create_player;
|
||||
return (bool) ($profile?->can_create_player ?? false);
|
||||
}
|
||||
|
||||
private function assertAgentProfileExists(AgentNode $agent): void
|
||||
|
||||
@@ -520,6 +520,15 @@ final class SettlementCenterLedgerService
|
||||
array_values($creditById),
|
||||
), static fn (int $id): bool => $id > 0)),
|
||||
);
|
||||
$creditBillRefs = $this->settlementBillsByIds(
|
||||
$admin,
|
||||
array_values(array_filter(array_map(
|
||||
static fn (object $row): int => (string) ($row->ref_type ?? '') === 'settlement_bill'
|
||||
? (int) ($row->ref_id ?? 0)
|
||||
: 0,
|
||||
array_values($creditById),
|
||||
), static fn (int $id): bool => $id > 0)),
|
||||
);
|
||||
|
||||
$paymentById = [];
|
||||
if ($paymentIds !== []) {
|
||||
@@ -556,7 +565,10 @@ final class SettlementCenterLedgerService
|
||||
if ($kind === 'credit' && isset($creditById[$id])) {
|
||||
$row = $creditById[$id];
|
||||
$pid = (int) $row->player_id;
|
||||
$items[] = $this->formatCreditEntry($row, $playerBills[$pid] ?? null, $ticketRefs);
|
||||
$bill = (string) ($row->ref_type ?? '') === 'settlement_bill'
|
||||
? ($creditBillRefs[(int) ($row->ref_id ?? 0)] ?? null)
|
||||
: ($playerBills[$pid] ?? null);
|
||||
$items[] = $this->formatCreditEntry($row, $bill, $ticketRefs);
|
||||
} elseif ($kind === 'payment' && isset($paymentById[$id])) {
|
||||
$items[] = $this->formatPaymentEntry($paymentById[$id]);
|
||||
} elseif ($kind === 'adjustment' && isset($adjustmentById[$id])) {
|
||||
@@ -569,6 +581,40 @@ final class SettlementCenterLedgerService
|
||||
return $items;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<int> $ids
|
||||
* @return array<int, object>
|
||||
*/
|
||||
private function settlementBillsByIds(AdminUser $admin, array $ids): array
|
||||
{
|
||||
$ids = array_values(array_unique(array_filter($ids, static fn (int $id): bool => $id > 0)));
|
||||
if ($ids === []) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$query = DB::table('settlement_bills as sb')
|
||||
->whereIn('sb.id', $ids)
|
||||
->select([
|
||||
'sb.id',
|
||||
'sb.owner_id as player_id',
|
||||
'sb.status',
|
||||
'sb.bill_type',
|
||||
'sb.net_amount',
|
||||
'sb.unpaid_amount',
|
||||
'sb.paid_amount',
|
||||
'sb.settlement_period_id',
|
||||
]);
|
||||
|
||||
AdminAgentSettlementScope::applySubtreeToBillsQuery($query, $admin, 'sb');
|
||||
|
||||
$map = [];
|
||||
foreach ($query->get() as $bill) {
|
||||
$map[(int) $bill->id] = $bill;
|
||||
}
|
||||
|
||||
return $map;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{0: Carbon|null, 1: Carbon|null}
|
||||
*/
|
||||
@@ -1273,6 +1319,8 @@ final class SettlementCenterLedgerService
|
||||
private function formatPaymentEntry(object $row): array
|
||||
{
|
||||
$amount = (int) $row->amount;
|
||||
$method = trim((string) ($row->method ?? ''));
|
||||
$methodLabel = $method !== '' && ! ctype_digit($method) ? ' · '.$method : '';
|
||||
|
||||
return array_merge(
|
||||
$this->baseRow(
|
||||
@@ -1289,7 +1337,7 @@ final class SettlementCenterLedgerService
|
||||
billStatus: (string) ($row->bill_status ?? ''),
|
||||
billType: (string) ($row->bill_type ?? ''),
|
||||
billUnpaid: isset($row->unpaid_amount) ? (int) $row->unpaid_amount : null,
|
||||
refLabel: 'bill#'.$row->settlement_bill_id.($row->method ? ' · '.$row->method : ''),
|
||||
refLabel: 'bill#'.$row->settlement_bill_id.$methodLabel,
|
||||
),
|
||||
$this->partyFieldsFromRow($row),
|
||||
);
|
||||
|
||||
@@ -104,8 +104,8 @@ final class AdminAuthProfile
|
||||
'code' => (string) $node->code,
|
||||
'name' => (string) $node->name,
|
||||
'depth' => (int) $node->depth,
|
||||
'can_create_child_agent' => $profile === null || $profile->can_create_child_agent,
|
||||
'can_create_player' => $profile === null || $profile->can_create_player,
|
||||
'can_create_child_agent' => (bool) ($profile?->can_create_child_agent ?? false),
|
||||
'can_create_player' => (bool) ($profile?->can_create_player ?? false),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -152,7 +152,7 @@ final class AdminAuthorizationRegistry
|
||||
['segment' => 'tickets', 'label' => 'Tickets', 'href' => '/admin/tickets', 'nav_group' => 'operations', 'requiredAny' => ['prd.tickets.view']],
|
||||
['segment' => 'players', 'label' => 'Players', 'href' => '/admin/players', 'nav_group' => 'operations', 'requiredAny' => ['prd.users.manage', 'prd.users.view_finance', 'prd.users.view_cs', 'prd.player_freeze.manage']],
|
||||
['segment' => 'settlement', 'label' => 'Settlement', 'href' => '/admin/settlement-batches', 'nav_group' => 'operations', 'requiredAny' => ['prd.payout.manage', 'prd.payout.review', 'prd.payout.view']],
|
||||
['segment' => 'wallet', 'label' => 'Wallet', 'href' => '/admin/wallet/transactions', 'nav_group' => 'finance', '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'], 'agent_hidden' => true],
|
||||
['segment' => 'wallet', 'label' => 'Wallet', 'href' => '/admin/wallet/transactions', 'nav_group' => 'finance', 'activeMatchPrefix' => '/admin/wallet', 'requiredAny' => ['prd.wallet_reconcile.manage', 'prd.wallet_reconcile.view', 'prd.wallet_reconcile.view_cs', 'prd.wallet_adjust.manage'], 'agent_hidden' => true],
|
||||
['segment' => 'reconcile', 'label' => 'Reconcile', 'href' => '/admin/reconcile', 'nav_group' => 'finance', 'requiredAny' => ['prd.wallet_reconcile.manage', 'prd.wallet_reconcile.view', 'prd.wallet_reconcile.view_cs'], 'agent_hidden' => true],
|
||||
['segment' => 'reports', 'label' => 'Reports', 'href' => '/admin/reports', 'nav_group' => 'finance', 'requiredAny' => ['prd.report.view']],
|
||||
['segment' => 'rules_plays', 'label' => 'Play rules', 'href' => '/admin/rules/plays', 'nav_group' => 'rules', 'platform_only' => true, 'requiredAny' => ['prd.play_switch.manage', 'prd.odds.manage', 'prd.odds.view']],
|
||||
@@ -164,7 +164,7 @@ final class AdminAuthorizationRegistry
|
||||
['segment' => 'admin_users', 'label' => 'Admin Users', 'href' => '/admin/admin-users', 'nav_group' => 'platform', 'platform_only' => true, 'requiredAny' => ['prd.admin_user.manage']],
|
||||
['segment' => 'admin_roles', 'label' => 'Admin Roles', 'href' => '/admin/admin-roles', 'nav_group' => 'platform', 'platform_only' => true, 'requiredAny' => ['prd.admin_role.manage']],
|
||||
['segment' => 'audit', 'label' => 'Audit Logs', 'href' => '/admin/audit-logs', 'nav_group' => 'platform', 'platform_only' => true, 'requiredAny' => ['prd.audit.view']],
|
||||
['segment' => 'settings', 'label' => 'Settings', 'href' => '/admin/settings', 'nav_group' => 'platform', 'platform_only' => true, 'requiredAny' => ['prd.wallet_reconcile.manage', 'prd.currency.manage']],
|
||||
['segment' => 'settings', 'label' => 'Settings', 'href' => '/admin/settings', 'nav_group' => 'platform', 'platform_only' => true, 'requiredAny' => ['prd.wallet_reconcile.manage', 'prd.rebate.manage', 'prd.rebate.view', 'prd.payout.manage']],
|
||||
['segment' => 'risk', 'label' => 'Risk', 'href' => '/admin/risk', 'nav_group' => 'platform', 'platform_only' => true, 'requiredAny' => ['prd.risk.view', 'prd.risk.manage']],
|
||||
];
|
||||
}
|
||||
@@ -477,8 +477,8 @@ final class AdminAuthorizationRegistry
|
||||
['code' => 'admin.config.risk-cap-versions.publish', 'module_code' => 'config', 'name' => '发布封顶版本', 'http_method' => 'POST', 'uri_pattern' => '/api/v1/admin/config/risk-cap-versions/{id}/publish', 'route_name' => 'api.v1.admin.config.risk-cap-versions.publish', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'legacy_permission_slugs' => ['prd.risk_cap.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.risk_cap.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', 'prd.rebate.manage', 'prd.rebate.view', 'prd.payout.manage']],
|
||||
['code' => 'admin.settings.batch-update', 'module_code' => 'settings', 'name' => '系统设置批量更新', 'http_method' => 'PUT', 'uri_pattern' => '/api/v1/admin/settings/batch', 'route_name' => 'api.v1.admin.settings.batch-update', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'legacy_permission_slugs' => ['prd.wallet_reconcile.manage', 'prd.rebate.manage', 'prd.payout.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', 'prd.rebate.manage', 'prd.payout.manage']],
|
||||
['code' => 'admin.settings.batch-update', 'module_code' => 'settings', 'name' => '系统设置批量更新', 'http_method' => 'PUT', 'uri_pattern' => '/api/v1/admin/settings/batch', 'route_name' => 'api.v1.admin.settings.batch-update', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'permission_codes' => ['service.wallet.manage', 'service.reconcile.manage', 'service.wallet.adjust', 'config.odds.manage', 'settlement.batch.manage', 'draw.review.review', 'draw.review.publish']],
|
||||
['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, 'permission_codes' => ['service.wallet.manage', 'service.reconcile.manage', 'service.wallet.adjust', 'config.odds.manage', 'settlement.batch.manage', 'draw.review.review', 'draw.review.publish']],
|
||||
['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']],
|
||||
@@ -489,7 +489,7 @@ final class AdminAuthorizationRegistry
|
||||
['code' => 'admin.integration-sites.show', 'module_code' => 'integration', 'name' => '接入站点详情', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/integration-sites/{admin_site}', 'route_name' => 'api.v1.admin.integration-sites.show', 'auth_mode' => 'permission_required', 'is_audit_required' => false, 'permission_codes' => ['integration.site.view', 'integration.site.manage']],
|
||||
['code' => 'admin.integration-sites.update', 'module_code' => 'integration', 'name' => '更新接入站点', 'http_method' => 'PUT', 'uri_pattern' => '/api/v1/admin/integration-sites/{admin_site}', 'route_name' => 'api.v1.admin.integration-sites.update', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'permission_codes' => ['integration.site.manage']],
|
||||
['code' => 'admin.integration-sites.rotate-secrets', 'module_code' => 'integration', 'name' => '重置接入密钥', 'http_method' => 'POST', 'uri_pattern' => '/api/v1/admin/integration-sites/{admin_site}/rotate-secrets', 'route_name' => 'api.v1.admin.integration-sites.rotate-secrets', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'permission_codes' => ['integration.site.manage']],
|
||||
['code' => 'admin.integration-sites.connectivity-test', 'module_code' => 'integration', 'name' => '接入站点联通检测', 'http_method' => 'POST', 'uri_pattern' => '/api/v1/admin/integration-sites/{admin_site}/connectivity-test', 'route_name' => 'api.v1.admin.integration-sites.connectivity-test', 'auth_mode' => 'permission_required', 'is_audit_required' => false, 'permission_codes' => ['integration.site.view', 'integration.site.manage']],
|
||||
['code' => 'admin.integration-sites.connectivity-test', 'module_code' => 'integration', 'name' => '接入站点联通检测', 'http_method' => 'POST', 'uri_pattern' => '/api/v1/admin/integration-sites/{admin_site}/connectivity-test', 'route_name' => 'api.v1.admin.integration-sites.connectivity-test', 'auth_mode' => 'permission_required', 'is_audit_required' => false, 'permission_codes' => ['integration.site.manage']],
|
||||
['code' => 'admin.integration-sites.export', 'module_code' => 'integration', 'name' => '导出接入参数表', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/integration-sites/{admin_site}/export', 'route_name' => 'api.v1.admin.integration-sites.export', 'auth_mode' => 'permission_required', 'is_audit_required' => false, 'permission_codes' => ['integration.site.view', 'integration.site.manage']],
|
||||
['code' => 'admin.integration-sites.secrets', 'module_code' => 'integration', 'name' => '查看接入密钥明文', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/integration-sites/{admin_site}/secrets', 'route_name' => 'api.v1.admin.integration-sites.secrets', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'permission_codes' => ['integration.site.manage']],
|
||||
|
||||
@@ -514,16 +514,16 @@ final class AdminAuthorizationRegistry
|
||||
['code' => 'admin.draws.risk-pools.recover', 'module_code' => 'risk', 'name' => '恢复风控池', 'http_method' => 'POST', 'uri_pattern' => '/api/v1/admin/draws/{draw}/risk-pools/{number_4d}/recover', 'route_name' => 'api.v1.admin.draws.risk-pools.recover', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'legacy_permission_slugs' => ['prd.draw_result.manage', 'prd.risk.manage']],
|
||||
['code' => 'admin.draws.cancel', 'module_code' => 'draw', 'name' => '取消开奖', 'http_method' => 'POST', 'uri_pattern' => '/api/v1/admin/draws/{draw}/cancel', 'route_name' => 'api.v1.admin.draws.cancel', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'legacy_permission_slugs' => ['prd.draw_result.manage']],
|
||||
['code' => 'admin.draws.rng', 'module_code' => 'draw', 'name' => '执行开奖 RNG', 'http_method' => 'POST', 'uri_pattern' => '/api/v1/admin/draws/{draw}/rng', 'route_name' => 'api.v1.admin.draws.rng', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'legacy_permission_slugs' => ['prd.draw_result.manage']],
|
||||
['code' => 'admin.draws.settlement.run', 'module_code' => 'settlement', 'name' => '执行结算', 'http_method' => 'POST', 'uri_pattern' => '/api/v1/admin/draws/{draw}/settlement/run', 'route_name' => 'api.v1.admin.draws.settlement.run', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'legacy_permission_slugs' => ['prd.payout.manage', 'prd.payout.review']],
|
||||
['code' => 'admin.draws.settlement.run', 'module_code' => 'settlement', 'name' => '执行结算', 'http_method' => 'POST', 'uri_pattern' => '/api/v1/admin/draws/{draw}/settlement/run', 'route_name' => 'api.v1.admin.draws.settlement.run', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'permission_codes' => ['settlement.batch.manage', 'settlement.batch.review']],
|
||||
|
||||
['code' => 'admin.settlement-batches.index', 'module_code' => 'settlement', 'name' => '结算批次列表', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/settlement-batches', 'route_name' => 'api.v1.admin.settlement-batches.index', 'auth_mode' => 'permission_required', 'is_audit_required' => false, 'legacy_permission_slugs' => ['prd.payout.manage', 'prd.payout.review', 'prd.payout.view']],
|
||||
['code' => 'admin.settlement-batches.show', 'module_code' => 'settlement', 'name' => '结算批次详情', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/settlement-batches/{batch}', 'route_name' => 'api.v1.admin.settlement-batches.show', 'auth_mode' => 'permission_required', 'is_audit_required' => false, 'legacy_permission_slugs' => ['prd.payout.manage', 'prd.payout.review', 'prd.payout.view']],
|
||||
['code' => 'admin.settlement-batches.details', 'module_code' => 'settlement', 'name' => '结算批次明细', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/settlement-batches/{batch}/details', 'route_name' => 'api.v1.admin.settlement-batches.details', 'auth_mode' => 'permission_required', 'is_audit_required' => false, 'legacy_permission_slugs' => ['prd.payout.manage', 'prd.payout.review', 'prd.payout.view']],
|
||||
['code' => 'admin.settlement-batches.export', 'module_code' => 'settlement', 'name' => '导出结算批次', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/settlement-batches/{batch}/export', 'route_name' => 'api.v1.admin.settlement-batches.export', 'auth_mode' => 'permission_required', 'is_audit_required' => false, 'legacy_permission_slugs' => ['prd.payout.manage', 'prd.payout.review', 'prd.payout.view']],
|
||||
['code' => 'admin.settlement-batches.approve', 'module_code' => 'settlement', 'name' => '审核结算批次', 'http_method' => 'POST', 'uri_pattern' => '/api/v1/admin/settlement-batches/{batch}/approve', 'route_name' => 'api.v1.admin.settlement-batches.approve', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'legacy_permission_slugs' => ['prd.payout.review']],
|
||||
['code' => 'admin.settlement-batches.reject', 'module_code' => 'settlement', 'name' => '驳回结算批次', 'http_method' => 'POST', 'uri_pattern' => '/api/v1/admin/settlement-batches/{batch}/reject', 'route_name' => 'api.v1.admin.settlement-batches.reject', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'legacy_permission_slugs' => ['prd.payout.review']],
|
||||
['code' => 'admin.settlement-batches.payout', 'module_code' => 'settlement', 'name' => '执行派彩', 'http_method' => 'POST', 'uri_pattern' => '/api/v1/admin/settlement-batches/{batch}/payout', 'route_name' => 'api.v1.admin.settlement-batches.payout', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'legacy_permission_slugs' => ['prd.payout.manage']],
|
||||
['code' => 'admin.settlement-batches.adjustments.store', 'module_code' => 'settlement', 'name' => '结算补差调账', 'http_method' => 'POST', 'uri_pattern' => '/api/v1/admin/settlement-batches/{batch}/adjustments', 'route_name' => 'api.v1.admin.settlement-batches.adjustments.store', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'legacy_permission_slugs' => ['prd.payout.manage']],
|
||||
['code' => 'admin.settlement-batches.approve', 'module_code' => 'settlement', 'name' => '审核结算批次', 'http_method' => 'POST', 'uri_pattern' => '/api/v1/admin/settlement-batches/{batch}/approve', 'route_name' => 'api.v1.admin.settlement-batches.approve', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'permission_codes' => ['settlement.batch.review']],
|
||||
['code' => 'admin.settlement-batches.reject', 'module_code' => 'settlement', 'name' => '驳回结算批次', 'http_method' => 'POST', 'uri_pattern' => '/api/v1/admin/settlement-batches/{batch}/reject', 'route_name' => 'api.v1.admin.settlement-batches.reject', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'permission_codes' => ['settlement.batch.review']],
|
||||
['code' => 'admin.settlement-batches.payout', 'module_code' => 'settlement', 'name' => '执行派彩', 'http_method' => 'POST', 'uri_pattern' => '/api/v1/admin/settlement-batches/{batch}/payout', 'route_name' => 'api.v1.admin.settlement-batches.payout', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'permission_codes' => ['settlement.batch.manage']],
|
||||
['code' => 'admin.settlement-batches.adjustments.store', 'module_code' => 'settlement', 'name' => '结算补差调账', 'http_method' => 'POST', 'uri_pattern' => '/api/v1/admin/settlement-batches/{batch}/adjustments', 'route_name' => 'api.v1.admin.settlement-batches.adjustments.store', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'permission_codes' => ['settlement.batch.manage']],
|
||||
|
||||
['code' => 'admin.jackpot.pools.index', 'module_code' => 'jackpot', 'name' => '奖池列表', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/jackpot/pools', 'route_name' => 'api.v1.admin.jackpot.pools.index', 'auth_mode' => 'permission_required', 'is_audit_required' => false, 'legacy_permission_slugs' => ['prd.jackpot.manage', 'prd.jackpot.view']],
|
||||
['code' => 'admin.jackpot.payout-logs.index', 'module_code' => 'jackpot', 'name' => '奖池派彩日志', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/jackpot/payout-logs', 'route_name' => 'api.v1.admin.jackpot.payout-logs.index', 'auth_mode' => 'permission_required', 'is_audit_required' => false, 'legacy_permission_slugs' => ['prd.jackpot.manage', 'prd.jackpot.view']],
|
||||
@@ -540,11 +540,11 @@ final class AdminAuthorizationRegistry
|
||||
['code' => 'admin.players.destroy', 'module_code' => 'player_service', 'name' => '删除玩家', 'http_method' => 'DELETE', 'uri_pattern' => '/api/v1/admin/players/{player}', 'route_name' => 'api.v1.admin.players.destroy', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'legacy_permission_slugs' => ['prd.users.manage']],
|
||||
['code' => 'admin.players.freeze', 'module_code' => 'player_service', 'name' => '冻结玩家', 'http_method' => 'POST', 'uri_pattern' => '/api/v1/admin/players/{player}/freeze', 'route_name' => 'api.v1.admin.players.freeze', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'permission_codes' => ['service.players.freeze']],
|
||||
['code' => 'admin.players.unfreeze', 'module_code' => 'player_service', 'name' => '解冻玩家', 'http_method' => 'POST', 'uri_pattern' => '/api/v1/admin/players/{player}/unfreeze', 'route_name' => 'api.v1.admin.players.unfreeze', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'permission_codes' => ['service.players.freeze']],
|
||||
['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.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.tickets.index', 'module_code' => 'ticket', 'name' => '后台注单列表', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/tickets', 'route_name' => 'api.v1.admin.tickets.index', 'auth_mode' => 'permission_required', 'is_audit_required' => false, 'legacy_permission_slugs' => ['prd.tickets.view']],
|
||||
|
||||
['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, 'legacy_permission_slugs' => ['prd.wallet_reconcile.manage', 'prd.wallet_reconcile.view', 'prd.wallet_reconcile.view_cs', 'prd.wallet_adjust.manage', 'prd.users.manage', 'prd.users.view_finance']],
|
||||
['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, 'legacy_permission_slugs' => ['prd.wallet_reconcile.manage', 'prd.wallet_reconcile.view', 'prd.wallet_reconcile.view_cs', 'prd.wallet_adjust.manage']],
|
||||
['code' => 'admin.wallet.transactions', 'module_code' => 'wallet', 'name' => '钱包流水查询', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/wallet/transactions', 'route_name' => 'api.v1.admin.wallet.transactions', '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.wallet.transfer-orders.reverse', 'module_code' => 'wallet', 'name' => '冲正转账单', 'http_method' => 'POST', 'uri_pattern' => '/api/v1/admin/wallet/transfer-orders/{transfer_no}/reverse', 'route_name' => 'api.v1.admin.wallet.transfer-orders.reverse', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'legacy_permission_slugs' => ['prd.wallet_adjust.manage', 'prd.wallet_reconcile.manage']],
|
||||
['code' => 'admin.wallet.transfer-orders.manually-process', 'module_code' => 'wallet', 'name' => '手工处理转账单', 'http_method' => 'POST', 'uri_pattern' => '/api/v1/admin/wallet/transfer-orders/{transfer_no}/manually-process', 'route_name' => 'api.v1.admin.wallet.transfer-orders.manually-process', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'legacy_permission_slugs' => ['prd.wallet_adjust.manage', 'prd.wallet_reconcile.manage']],
|
||||
|
||||
83
app/Support/AdminSettingPolicy.php
Normal file
83
app/Support/AdminSettingPolicy.php
Normal file
@@ -0,0 +1,83 @@
|
||||
<?php
|
||||
|
||||
namespace App\Support;
|
||||
|
||||
use App\Models\AdminUser;
|
||||
use App\Services\LotterySettings;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
|
||||
final class AdminSettingPolicy
|
||||
{
|
||||
private const WALLET_LIMIT_KEYS = [
|
||||
'wallet.transfer_in_min_minor',
|
||||
'wallet.transfer_in_max_minor',
|
||||
'wallet.transfer_out_min_minor',
|
||||
'wallet.transfer_out_max_minor',
|
||||
];
|
||||
|
||||
public static function canUpdate(AdminUser $admin, string $key): bool
|
||||
{
|
||||
if (str_starts_with($key, 'settlement.')) {
|
||||
return $admin->hasAdminPermission('prd.payout.manage');
|
||||
}
|
||||
|
||||
if (str_starts_with($key, 'draw.')) {
|
||||
return $admin->hasAdminPermission('prd.draw_result.manage');
|
||||
}
|
||||
|
||||
if (str_starts_with($key, 'frontend.')) {
|
||||
return $admin->hasAdminPermission('prd.odds.manage')
|
||||
|| $admin->hasAdminPermission('prd.rebate.manage');
|
||||
}
|
||||
|
||||
if (str_starts_with($key, 'wallet.')) {
|
||||
return $admin->hasAdminPermission('prd.wallet_reconcile.manage')
|
||||
|| $admin->hasAdminPermission('prd.wallet_adjust.manage');
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<array{key: string, value: mixed}> $items
|
||||
*/
|
||||
public static function validateItems(array $items): void
|
||||
{
|
||||
$values = [];
|
||||
foreach ($items as $index => $item) {
|
||||
$key = (string) $item['key'];
|
||||
if (! in_array($key, self::WALLET_LIMIT_KEYS, true)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$value = $item['value'];
|
||||
if (! is_int($value) || $value < 1) {
|
||||
throw ValidationException::withMessages([
|
||||
"items.$index.value" => ['钱包转账限额必须是大于等于 1 的整数最小货币单位。'],
|
||||
]);
|
||||
}
|
||||
|
||||
$values[$key] = $value;
|
||||
}
|
||||
|
||||
self::validateWalletRange($values, 'wallet.transfer_in_min_minor', 'wallet.transfer_in_max_minor');
|
||||
self::validateWalletRange($values, 'wallet.transfer_out_min_minor', 'wallet.transfer_out_max_minor');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, int> $values
|
||||
*/
|
||||
private static function validateWalletRange(array $values, string $minKey, string $maxKey): void
|
||||
{
|
||||
$min = $values[$minKey] ?? max(1, (int) LotterySettings::get($minKey, 1));
|
||||
$max = $values[$maxKey] ?? max(1, (int) LotterySettings::get($maxKey, 1));
|
||||
|
||||
if ($max >= $min) {
|
||||
return;
|
||||
}
|
||||
|
||||
throw ValidationException::withMessages([
|
||||
'items' => ['钱包转账最大金额不能小于最小金额。'],
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -32,7 +32,6 @@ final class AgentDefaultRolePermissions
|
||||
|
||||
private const PLAYER_MANAGE_SLUGS = [
|
||||
'prd.users.manage',
|
||||
'prd.users.view_finance',
|
||||
'prd.users.view_cs',
|
||||
];
|
||||
|
||||
@@ -43,7 +42,6 @@ final class AgentDefaultRolePermissions
|
||||
'prd.agent.role.manage',
|
||||
'prd.agent.user.manage',
|
||||
'prd.users.manage',
|
||||
'prd.users.view_finance',
|
||||
'prd.users.view_cs',
|
||||
'prd.settlement.agent.manage',
|
||||
];
|
||||
@@ -106,10 +104,7 @@ final class AgentDefaultRolePermissions
|
||||
*/
|
||||
public static function defaultOwnerSlugsWithoutProfile(): array
|
||||
{
|
||||
return array_values(array_unique(array_merge(
|
||||
self::BASE_SLUGS,
|
||||
self::PLAYER_MANAGE_SLUGS,
|
||||
)));
|
||||
return self::BASE_SLUGS;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -32,7 +32,6 @@ final class AgentProfileCapabilityFilter
|
||||
/** @var list<string> */
|
||||
private const PLAYER_LEGACY_SLUGS = [
|
||||
'prd.users.manage',
|
||||
'prd.users.view_finance',
|
||||
'prd.users.view_cs',
|
||||
'prd.player_freeze.manage',
|
||||
];
|
||||
@@ -45,10 +44,6 @@ final class AgentProfileCapabilityFilter
|
||||
*/
|
||||
public static function applyToMenuActionCodes(array $permissionCodes, ?AgentProfile $profile): array
|
||||
{
|
||||
if ($profile === null) {
|
||||
return $permissionCodes;
|
||||
}
|
||||
|
||||
$set = [];
|
||||
foreach ($permissionCodes as $code) {
|
||||
if (is_string($code) && $code !== '') {
|
||||
@@ -56,7 +51,7 @@ final class AgentProfileCapabilityFilter
|
||||
}
|
||||
}
|
||||
|
||||
if (! $profile->can_create_child_agent) {
|
||||
if (! ($profile?->can_create_child_agent ?? false)) {
|
||||
foreach (self::CHILD_AGENT_PERMISSION_CODES as $code) {
|
||||
unset($set[$code]);
|
||||
}
|
||||
@@ -66,7 +61,7 @@ final class AgentProfileCapabilityFilter
|
||||
}
|
||||
}
|
||||
|
||||
if (! $profile->can_create_player) {
|
||||
if (! ($profile?->can_create_player ?? false)) {
|
||||
foreach (self::PLAYER_PERMISSION_CODES as $code) {
|
||||
unset($set[$code]);
|
||||
}
|
||||
@@ -97,15 +92,11 @@ final class AgentProfileCapabilityFilter
|
||||
*/
|
||||
public static function filterLegacySlugs(array $legacySlugs, ?AgentProfile $profile): array
|
||||
{
|
||||
if ($profile === null) {
|
||||
return $legacySlugs;
|
||||
}
|
||||
|
||||
$deny = [];
|
||||
if (! $profile->can_create_child_agent) {
|
||||
if (! ($profile?->can_create_child_agent ?? false)) {
|
||||
$deny = array_merge($deny, self::CHILD_AGENT_LEGACY_SLUGS);
|
||||
}
|
||||
if (! $profile->can_create_player) {
|
||||
if (! ($profile?->can_create_player ?? false)) {
|
||||
$deny = array_merge($deny, self::PLAYER_LEGACY_SLUGS);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user