feat: 增强后台设置校验、代理权限控制与财务审计能力
This commit is contained in:
@@ -78,7 +78,7 @@ final class AuditAgentLineDataCommand extends Command
|
|||||||
->select('admin_site_id', DB::raw('count(*) as cnt'))
|
->select('admin_site_id', DB::raw('count(*) as cnt'))
|
||||||
->where('depth', '>', 0)
|
->where('depth', '>', 0)
|
||||||
->groupBy('admin_site_id')
|
->groupBy('admin_site_id')
|
||||||
->having('cnt', '>', 0)
|
->havingRaw('count(*) > 0')
|
||||||
->get();
|
->get();
|
||||||
|
|
||||||
foreach ($sitesWithManyBusinessAgents as $row) {
|
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\Services\Agent\AgentAdminUserService;
|
||||||
use App\Lottery\ErrorCode;
|
use App\Lottery\ErrorCode;
|
||||||
use App\Support\AdminAgentNodeAccess;
|
use App\Support\AdminAgentNodeAccess;
|
||||||
|
use App\Support\AdminAgentScope;
|
||||||
use App\Support\AdminUserApiPresenter;
|
use App\Support\AdminUserApiPresenter;
|
||||||
use App\Support\ApiMessage;
|
use App\Support\ApiMessage;
|
||||||
use App\Http\Requests\Admin\AgentAdminUserStoreRequest;
|
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());
|
$user = $service->createUnderAgent($agent_node, $request->validated());
|
||||||
|
|
||||||
AuditLogger::recordForAdmin(
|
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);
|
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);
|
$before = AgentNodePresenter::item($agent_node);
|
||||||
$service->destroy($agent_node);
|
$service->destroy($agent_node);
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ use App\Http\Controllers\Controller;
|
|||||||
use App\Services\Agent\AgentRoleService;
|
use App\Services\Agent\AgentRoleService;
|
||||||
use App\Lottery\ErrorCode;
|
use App\Lottery\ErrorCode;
|
||||||
use App\Support\AdminAgentNodeAccess;
|
use App\Support\AdminAgentNodeAccess;
|
||||||
|
use App\Support\AdminAgentScope;
|
||||||
use App\Support\AdminRoleApiPresenter;
|
use App\Support\AdminRoleApiPresenter;
|
||||||
use App\Support\ApiMessage;
|
use App\Support\ApiMessage;
|
||||||
use App\Http\Requests\Admin\AgentRoleStoreRequest;
|
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());
|
$role = $service->createForAgent($admin, $agent_node, $request->validated());
|
||||||
|
|
||||||
AuditLogger::recordForAdmin(
|
AuditLogger::recordForAdmin(
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ namespace App\Http\Requests\Admin;
|
|||||||
|
|
||||||
use App\Models\AdminUser;
|
use App\Models\AdminUser;
|
||||||
use App\Http\Requests\ApiFormRequest;
|
use App\Http\Requests\ApiFormRequest;
|
||||||
|
use App\Support\AdminSettingPolicy;
|
||||||
|
|
||||||
final class AdminSettingBatchUpdateRequest extends ApiFormRequest
|
final class AdminSettingBatchUpdateRequest extends ApiFormRequest
|
||||||
{
|
{
|
||||||
@@ -22,8 +23,8 @@ final class AdminSettingBatchUpdateRequest extends ApiFormRequest
|
|||||||
|
|
||||||
foreach ($items as $item) {
|
foreach ($items as $item) {
|
||||||
$key = is_array($item) ? (string) ($item['key'] ?? '') : '';
|
$key = is_array($item) ? (string) ($item['key'] ?? '') : '';
|
||||||
if (str_starts_with($key, 'settlement.')) {
|
if (! AdminSettingPolicy::canUpdate($admin, $key)) {
|
||||||
return $admin->hasAdminPermission('prd.payout.manage');
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -38,4 +39,15 @@ final class AdminSettingBatchUpdateRequest extends ApiFormRequest
|
|||||||
'items.*.value' => ['present'],
|
'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\Models\AdminUser;
|
||||||
use App\Http\Requests\ApiFormRequest;
|
use App\Http\Requests\ApiFormRequest;
|
||||||
|
use App\Support\AdminSettingPolicy;
|
||||||
|
|
||||||
final class AdminSettingUpdateRequest extends ApiFormRequest
|
final class AdminSettingUpdateRequest extends ApiFormRequest
|
||||||
{
|
{
|
||||||
@@ -15,11 +16,7 @@ final class AdminSettingUpdateRequest extends ApiFormRequest
|
|||||||
}
|
}
|
||||||
|
|
||||||
$key = (string) $this->route('key', '');
|
$key = (string) $this->route('key', '');
|
||||||
if (str_starts_with($key, 'settlement.')) {
|
return AdminSettingPolicy::canUpdate($admin, $key);
|
||||||
return $admin->hasAdminPermission('prd.payout.manage');
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function rules(): array
|
public function rules(): array
|
||||||
@@ -28,4 +25,17 @@ final class AdminSettingUpdateRequest extends ApiFormRequest
|
|||||||
'value' => ['present'],
|
'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 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')
|
return $admin->hasPermissionCode('service.reconcile.manage')
|
||||||
|| $admin->hasPermissionCode('service.reconcile.view')
|
|| $admin->hasPermissionCode('service.reconcile.view')
|
||||||
|| $admin->hasPermissionCode('service.wallet.view')
|
|
||||||
|| $admin->hasPermissionCode('service.wallet.manage');
|
|| $admin->hasPermissionCode('service.wallet.manage');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,13 +6,20 @@ use App\Models\AdminUser;
|
|||||||
use App\Models\AgentNode;
|
use App\Models\AgentNode;
|
||||||
use App\Models\AgentProfile;
|
use App\Models\AgentProfile;
|
||||||
use App\Models\Player;
|
use App\Models\Player;
|
||||||
|
use App\Support\AdminScopeContext;
|
||||||
|
use App\Support\AdminScopeContextResolver;
|
||||||
use App\Support\AdminAgentSettlementScope;
|
use App\Support\AdminAgentSettlementScope;
|
||||||
|
use Carbon\Carbon;
|
||||||
use Illuminate\Support\Facades\DB;
|
use Illuminate\Support\Facades\DB;
|
||||||
use Illuminate\Support\Facades\Schema;
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
/** 代理账号仪表盘:授信、团队规模、待结账单摘要。 */
|
/** 代理账号仪表盘:授信、团队规模、待结账单摘要。 */
|
||||||
final class AgentDashboardOverviewBuilder
|
final class AgentDashboardOverviewBuilder
|
||||||
{
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly AdminReportQueryService $reportQuery,
|
||||||
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return array<string, mixed>|null
|
* @return array<string, mixed>|null
|
||||||
*/
|
*/
|
||||||
@@ -46,6 +53,16 @@ final class AgentDashboardOverviewBuilder
|
|||||||
}
|
}
|
||||||
|
|
||||||
$pendingBillStats = $this->pendingBillStats($admin, $subtreeIds);
|
$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 [
|
return [
|
||||||
'agent_node_id' => (int) $node->id,
|
'agent_node_id' => (int) $node->id,
|
||||||
@@ -61,13 +78,25 @@ final class AgentDashboardOverviewBuilder
|
|||||||
),
|
),
|
||||||
'total_share_rate' => (float) ($profile?->total_share_rate ?? 0),
|
'total_share_rate' => (float) ($profile?->total_share_rate ?? 0),
|
||||||
'settlement_cycle' => (string) ($profile?->settlement_cycle ?? 'weekly'),
|
'settlement_cycle' => (string) ($profile?->settlement_cycle ?? 'weekly'),
|
||||||
'can_create_child_agent' => $profile === null || $profile->can_create_child_agent,
|
'can_create_child_agent' => (bool) ($profile?->can_create_child_agent ?? false),
|
||||||
'can_create_player' => $profile === null || $profile->can_create_player,
|
'can_create_player' => (bool) ($profile?->can_create_player ?? false),
|
||||||
'direct_child_count' => $directChildCount,
|
'direct_child_count' => $directChildCount,
|
||||||
'subtree_agent_count' => count($subtreeIds),
|
'subtree_agent_count' => count($subtreeIds),
|
||||||
'direct_player_count' => $directPlayerCount,
|
'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_bill_count' => $pendingBillStats['count'],
|
||||||
'pending_unpaid_minor' => $pendingBillStats['unpaid_minor'],
|
'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'),
|
'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);
|
$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
|
public function nodeMayCreatePlayer(int $agentNodeId): bool
|
||||||
{
|
{
|
||||||
$profile = $this->profileForNode($agentNodeId);
|
$profile = $this->profileForNode($agentNodeId);
|
||||||
|
|
||||||
return $profile === null || $profile->can_create_player;
|
return (bool) ($profile?->can_create_player ?? false);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function assertAgentProfileExists(AgentNode $agent): void
|
private function assertAgentProfileExists(AgentNode $agent): void
|
||||||
|
|||||||
@@ -520,6 +520,15 @@ final class SettlementCenterLedgerService
|
|||||||
array_values($creditById),
|
array_values($creditById),
|
||||||
), static fn (int $id): bool => $id > 0)),
|
), 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 = [];
|
$paymentById = [];
|
||||||
if ($paymentIds !== []) {
|
if ($paymentIds !== []) {
|
||||||
@@ -556,7 +565,10 @@ final class SettlementCenterLedgerService
|
|||||||
if ($kind === 'credit' && isset($creditById[$id])) {
|
if ($kind === 'credit' && isset($creditById[$id])) {
|
||||||
$row = $creditById[$id];
|
$row = $creditById[$id];
|
||||||
$pid = (int) $row->player_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])) {
|
} elseif ($kind === 'payment' && isset($paymentById[$id])) {
|
||||||
$items[] = $this->formatPaymentEntry($paymentById[$id]);
|
$items[] = $this->formatPaymentEntry($paymentById[$id]);
|
||||||
} elseif ($kind === 'adjustment' && isset($adjustmentById[$id])) {
|
} elseif ($kind === 'adjustment' && isset($adjustmentById[$id])) {
|
||||||
@@ -569,6 +581,40 @@ final class SettlementCenterLedgerService
|
|||||||
return $items;
|
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}
|
* @return array{0: Carbon|null, 1: Carbon|null}
|
||||||
*/
|
*/
|
||||||
@@ -1273,6 +1319,8 @@ final class SettlementCenterLedgerService
|
|||||||
private function formatPaymentEntry(object $row): array
|
private function formatPaymentEntry(object $row): array
|
||||||
{
|
{
|
||||||
$amount = (int) $row->amount;
|
$amount = (int) $row->amount;
|
||||||
|
$method = trim((string) ($row->method ?? ''));
|
||||||
|
$methodLabel = $method !== '' && ! ctype_digit($method) ? ' · '.$method : '';
|
||||||
|
|
||||||
return array_merge(
|
return array_merge(
|
||||||
$this->baseRow(
|
$this->baseRow(
|
||||||
@@ -1289,7 +1337,7 @@ final class SettlementCenterLedgerService
|
|||||||
billStatus: (string) ($row->bill_status ?? ''),
|
billStatus: (string) ($row->bill_status ?? ''),
|
||||||
billType: (string) ($row->bill_type ?? ''),
|
billType: (string) ($row->bill_type ?? ''),
|
||||||
billUnpaid: isset($row->unpaid_amount) ? (int) $row->unpaid_amount : null,
|
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),
|
$this->partyFieldsFromRow($row),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -104,8 +104,8 @@ final class AdminAuthProfile
|
|||||||
'code' => (string) $node->code,
|
'code' => (string) $node->code,
|
||||||
'name' => (string) $node->name,
|
'name' => (string) $node->name,
|
||||||
'depth' => (int) $node->depth,
|
'depth' => (int) $node->depth,
|
||||||
'can_create_child_agent' => $profile === null || $profile->can_create_child_agent,
|
'can_create_child_agent' => (bool) ($profile?->can_create_child_agent ?? false),
|
||||||
'can_create_player' => $profile === null || $profile->can_create_player,
|
'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' => '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' => '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' => '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' => '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' => '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']],
|
['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_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' => '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' => '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']],
|
['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.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.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.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.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, '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, '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.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.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.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.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.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.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.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']],
|
['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.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.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.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.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.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.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.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.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, '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, '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, 'legacy_permission_slugs' => ['prd.payout.manage']],
|
['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, '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, '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.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']],
|
['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.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.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.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.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.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.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.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']],
|
['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 = [
|
private const PLAYER_MANAGE_SLUGS = [
|
||||||
'prd.users.manage',
|
'prd.users.manage',
|
||||||
'prd.users.view_finance',
|
|
||||||
'prd.users.view_cs',
|
'prd.users.view_cs',
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -43,7 +42,6 @@ final class AgentDefaultRolePermissions
|
|||||||
'prd.agent.role.manage',
|
'prd.agent.role.manage',
|
||||||
'prd.agent.user.manage',
|
'prd.agent.user.manage',
|
||||||
'prd.users.manage',
|
'prd.users.manage',
|
||||||
'prd.users.view_finance',
|
|
||||||
'prd.users.view_cs',
|
'prd.users.view_cs',
|
||||||
'prd.settlement.agent.manage',
|
'prd.settlement.agent.manage',
|
||||||
];
|
];
|
||||||
@@ -106,10 +104,7 @@ final class AgentDefaultRolePermissions
|
|||||||
*/
|
*/
|
||||||
public static function defaultOwnerSlugsWithoutProfile(): array
|
public static function defaultOwnerSlugsWithoutProfile(): array
|
||||||
{
|
{
|
||||||
return array_values(array_unique(array_merge(
|
return self::BASE_SLUGS;
|
||||||
self::BASE_SLUGS,
|
|
||||||
self::PLAYER_MANAGE_SLUGS,
|
|
||||||
)));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -32,7 +32,6 @@ final class AgentProfileCapabilityFilter
|
|||||||
/** @var list<string> */
|
/** @var list<string> */
|
||||||
private const PLAYER_LEGACY_SLUGS = [
|
private const PLAYER_LEGACY_SLUGS = [
|
||||||
'prd.users.manage',
|
'prd.users.manage',
|
||||||
'prd.users.view_finance',
|
|
||||||
'prd.users.view_cs',
|
'prd.users.view_cs',
|
||||||
'prd.player_freeze.manage',
|
'prd.player_freeze.manage',
|
||||||
];
|
];
|
||||||
@@ -45,10 +44,6 @@ final class AgentProfileCapabilityFilter
|
|||||||
*/
|
*/
|
||||||
public static function applyToMenuActionCodes(array $permissionCodes, ?AgentProfile $profile): array
|
public static function applyToMenuActionCodes(array $permissionCodes, ?AgentProfile $profile): array
|
||||||
{
|
{
|
||||||
if ($profile === null) {
|
|
||||||
return $permissionCodes;
|
|
||||||
}
|
|
||||||
|
|
||||||
$set = [];
|
$set = [];
|
||||||
foreach ($permissionCodes as $code) {
|
foreach ($permissionCodes as $code) {
|
||||||
if (is_string($code) && $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) {
|
foreach (self::CHILD_AGENT_PERMISSION_CODES as $code) {
|
||||||
unset($set[$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) {
|
foreach (self::PLAYER_PERMISSION_CODES as $code) {
|
||||||
unset($set[$code]);
|
unset($set[$code]);
|
||||||
}
|
}
|
||||||
@@ -97,15 +92,11 @@ final class AgentProfileCapabilityFilter
|
|||||||
*/
|
*/
|
||||||
public static function filterLegacySlugs(array $legacySlugs, ?AgentProfile $profile): array
|
public static function filterLegacySlugs(array $legacySlugs, ?AgentProfile $profile): array
|
||||||
{
|
{
|
||||||
if ($profile === null) {
|
|
||||||
return $legacySlugs;
|
|
||||||
}
|
|
||||||
|
|
||||||
$deny = [];
|
$deny = [];
|
||||||
if (! $profile->can_create_child_agent) {
|
if (! ($profile?->can_create_child_agent ?? false)) {
|
||||||
$deny = array_merge($deny, self::CHILD_AGENT_LEGACY_SLUGS);
|
$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);
|
$deny = array_merge($deny, self::PLAYER_LEGACY_SLUGS);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ use Illuminate\Foundation\Testing\RefreshDatabase;
|
|||||||
uses(RefreshDatabase::class);
|
uses(RefreshDatabase::class);
|
||||||
|
|
||||||
beforeEach(function (): void {
|
beforeEach(function (): void {
|
||||||
|
ensureAdminActionCatalogSeeded();
|
||||||
$this->artisan('lottery:admin-auth-sync')->assertExitCode(0);
|
$this->artisan('lottery:admin-auth-sync')->assertExitCode(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ use Illuminate\Foundation\Testing\RefreshDatabase;
|
|||||||
uses(RefreshDatabase::class);
|
uses(RefreshDatabase::class);
|
||||||
|
|
||||||
beforeEach(function (): void {
|
beforeEach(function (): void {
|
||||||
|
ensureAdminActionCatalogSeeded();
|
||||||
$this->artisan('lottery:admin-auth-sync')->assertExitCode(0);
|
$this->artisan('lottery:admin-auth-sync')->assertExitCode(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ use Illuminate\Foundation\Testing\RefreshDatabase;
|
|||||||
uses(RefreshDatabase::class);
|
uses(RefreshDatabase::class);
|
||||||
|
|
||||||
beforeEach(function (): void {
|
beforeEach(function (): void {
|
||||||
|
ensureAdminActionCatalogSeeded();
|
||||||
$this->artisan('lottery:admin-auth-sync')->assertExitCode(0);
|
$this->artisan('lottery:admin-auth-sync')->assertExitCode(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -209,6 +210,58 @@ test('agent operator can create child under own node but not under sibling', fun
|
|||||||
->assertForbidden();
|
->assertForbidden();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('agent operator cannot create role or admin user under descendant node', function (): void {
|
||||||
|
$siteId = (int) DB::table('admin_sites')->where('is_default', true)->value('id');
|
||||||
|
$rootId = agentRootNodeId($siteId);
|
||||||
|
$service = app(\App\Services\Agent\AgentNodeService::class);
|
||||||
|
$super = AdminUser::query()->create([
|
||||||
|
'username' => 'bootstrap4',
|
||||||
|
'name' => 'Bootstrap',
|
||||||
|
'email' => null,
|
||||||
|
'password' => Hash::make('secret-strong'),
|
||||||
|
'status' => 0,
|
||||||
|
]);
|
||||||
|
grantSuperAdminRole($super);
|
||||||
|
|
||||||
|
$nodeA = $service->createChild($super, agentChildPayload([
|
||||||
|
'parent_id' => $rootId,
|
||||||
|
'code' => 'branch-a3',
|
||||||
|
'name' => 'Branch A3',
|
||||||
|
]));
|
||||||
|
$child = $service->createChild($super, agentChildPayload([
|
||||||
|
'parent_id' => $nodeA->id,
|
||||||
|
'code' => 'branch-a3-child',
|
||||||
|
'name' => 'Branch A3 Child',
|
||||||
|
]));
|
||||||
|
|
||||||
|
$operator = AdminUser::query()->create([
|
||||||
|
'username' => 'agent_a3_ops',
|
||||||
|
'name' => 'A3 Ops',
|
||||||
|
'email' => null,
|
||||||
|
'password' => Hash::make('secret-strong'),
|
||||||
|
'status' => 0,
|
||||||
|
]);
|
||||||
|
grantAgentOperatorRole($operator, $nodeA);
|
||||||
|
$token = $operator->createToken('test', ['*'], now()->addDay())->plainTextToken;
|
||||||
|
|
||||||
|
$this->withHeader('Authorization', 'Bearer '.$token)
|
||||||
|
->postJson('/api/v1/admin/agent-nodes/'.$child->id.'/roles', [
|
||||||
|
'slug' => 'child_role',
|
||||||
|
'name' => 'Child Role',
|
||||||
|
'permission_slugs' => ['prd.agent.view'],
|
||||||
|
])
|
||||||
|
->assertForbidden();
|
||||||
|
|
||||||
|
$this->withHeader('Authorization', 'Bearer '.$token)
|
||||||
|
->postJson('/api/v1/admin/agent-nodes/'.$child->id.'/admin-users', [
|
||||||
|
'username' => 'child_admin',
|
||||||
|
'nickname' => 'Child Admin',
|
||||||
|
'password' => 'Secret123!',
|
||||||
|
'role_ids' => [],
|
||||||
|
])
|
||||||
|
->assertForbidden();
|
||||||
|
});
|
||||||
|
|
||||||
test('auth me returns agent context for bound operator', function (): void {
|
test('auth me returns agent context for bound operator', function (): void {
|
||||||
$siteId = (int) DB::table('admin_sites')->where('is_default', true)->value('id');
|
$siteId = (int) DB::table('admin_sites')->where('is_default', true)->value('id');
|
||||||
$rootId = agentRootNodeId($siteId);
|
$rootId = agentRootNodeId($siteId);
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ use Illuminate\Foundation\Testing\RefreshDatabase;
|
|||||||
uses(RefreshDatabase::class);
|
uses(RefreshDatabase::class);
|
||||||
|
|
||||||
beforeEach(function (): void {
|
beforeEach(function (): void {
|
||||||
|
ensureAdminActionCatalogSeeded();
|
||||||
$this->artisan('lottery:admin-auth-sync')->assertExitCode(0);
|
$this->artisan('lottery:admin-auth-sync')->assertExitCode(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -152,6 +152,89 @@ test('credit ledger index includes payment on agent bill', function (): void {
|
|||||||
->assertJsonPath('data.items.0.bill_type', 'agent');
|
->assertJsonPath('data.items.0.bill_type', 'agent');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('credit ledger settlement bill reference keeps the referenced bill id', function (): void {
|
||||||
|
$site = DB::table('admin_sites')->where('is_default', true)->first();
|
||||||
|
$siteId = (int) $site->id;
|
||||||
|
$siteCode = (string) $site->code;
|
||||||
|
|
||||||
|
$periodId = (int) DB::table('settlement_periods')->insertGetId([
|
||||||
|
'admin_site_id' => $siteId,
|
||||||
|
'period_start' => now()->subDay(),
|
||||||
|
'period_end' => now()->addDay(),
|
||||||
|
'status' => 'closed',
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$player = Player::query()->create([
|
||||||
|
'site_code' => $siteCode,
|
||||||
|
'site_player_id' => 'native:bill-ref-ledger',
|
||||||
|
'funding_mode' => PlayerFundingMode::CREDIT,
|
||||||
|
'username' => 'bill_ref_ledger_user',
|
||||||
|
'default_currency' => 'NPR',
|
||||||
|
'status' => 0,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$referencedBillId = (int) DB::table('settlement_bills')->insertGetId([
|
||||||
|
'settlement_period_id' => $periodId,
|
||||||
|
'bill_type' => 'player',
|
||||||
|
'owner_type' => 'player',
|
||||||
|
'owner_id' => $player->id,
|
||||||
|
'counterparty_type' => 'agent',
|
||||||
|
'counterparty_id' => 1,
|
||||||
|
'net_amount' => -100,
|
||||||
|
'unpaid_amount' => 0,
|
||||||
|
'paid_amount' => 100,
|
||||||
|
'status' => 'settled',
|
||||||
|
'created_at' => now()->subMinute(),
|
||||||
|
'updated_at' => now()->subMinute(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$newerBillId = (int) DB::table('settlement_bills')->insertGetId([
|
||||||
|
'settlement_period_id' => $periodId,
|
||||||
|
'bill_type' => 'player',
|
||||||
|
'owner_type' => 'player',
|
||||||
|
'owner_id' => $player->id,
|
||||||
|
'counterparty_type' => 'agent',
|
||||||
|
'counterparty_id' => 1,
|
||||||
|
'net_amount' => -200,
|
||||||
|
'unpaid_amount' => 0,
|
||||||
|
'paid_amount' => 200,
|
||||||
|
'status' => 'settled',
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
DB::table('credit_ledger')->insert([
|
||||||
|
'owner_type' => 'player',
|
||||||
|
'owner_id' => $player->id,
|
||||||
|
'amount' => 100,
|
||||||
|
'reason' => 'settlement_payout',
|
||||||
|
'ref_type' => 'settlement_bill',
|
||||||
|
'ref_id' => $referencedBillId,
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$admin = AdminUser::query()->create([
|
||||||
|
'username' => 'ledger_bill_ref_super',
|
||||||
|
'name' => 'Ledger Bill Ref',
|
||||||
|
'email' => null,
|
||||||
|
'password' => Hash::make('secret-strong'),
|
||||||
|
'status' => 0,
|
||||||
|
]);
|
||||||
|
grantSuperAdminRole($admin);
|
||||||
|
$token = $admin->createToken('test', ['*'], now()->addDay())->plainTextToken;
|
||||||
|
|
||||||
|
$this->withHeader('Authorization', 'Bearer '.$token)
|
||||||
|
->getJson('/api/v1/admin/credit-ledger?admin_site_id='.$siteId.'&settlement_period_id='.$periodId.'&reason=settlement_payout')
|
||||||
|
->assertOk()
|
||||||
|
->assertJsonPath('data.total', 1)
|
||||||
|
->assertJsonPath('data.items.0.entry_kind', 'credit')
|
||||||
|
->assertJsonPath('data.items.0.settlement_bill_id', $referencedBillId)
|
||||||
|
->assertJsonPath('data.items.0.ref_id', $referencedBillId);
|
||||||
|
});
|
||||||
|
|
||||||
test('credit ledger entry_kind share returns share ledger rows', function (): void {
|
test('credit ledger entry_kind share returns share ledger rows', function (): void {
|
||||||
$site = DB::table('admin_sites')->where('is_default', true)->first();
|
$site = DB::table('admin_sites')->where('is_default', true)->first();
|
||||||
$siteId = (int) $site->id;
|
$siteId = (int) $site->id;
|
||||||
|
|||||||
@@ -157,3 +157,56 @@ test('non payout manager cannot update single settlement setting', function ():
|
|||||||
|
|
||||||
expect(LotterySetting::query()->where('setting_key', 'settlement.auto_approve_on_tick')->value('value_json'))->toBeTrue();
|
expect(LotterySetting::query()->where('setting_key', 'settlement.auto_approve_on_tick')->value('value_json'))->toBeTrue();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('wallet limits must be positive integer minor units', function (): void {
|
||||||
|
LotterySettings::put('wallet.transfer_in_min_minor', 100, 'wallet');
|
||||||
|
|
||||||
|
$token = settingsAdminToken();
|
||||||
|
|
||||||
|
$this->withHeader('Authorization', 'Bearer '.$token)
|
||||||
|
->putJson('/api/v1/admin/settings/batch', [
|
||||||
|
'items' => [
|
||||||
|
['key' => 'wallet.transfer_in_min_minor', 'value' => 0],
|
||||||
|
],
|
||||||
|
])
|
||||||
|
->assertUnprocessable();
|
||||||
|
|
||||||
|
expect(LotterySetting::query()->where('setting_key', 'wallet.transfer_in_min_minor')->value('value_json'))->toBe(100);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('wallet max limit cannot be less than current min limit', function (): void {
|
||||||
|
LotterySettings::put('wallet.transfer_in_min_minor', 100, 'wallet');
|
||||||
|
LotterySettings::put('wallet.transfer_in_max_minor', 10_000, 'wallet');
|
||||||
|
|
||||||
|
$token = settingsAdminToken();
|
||||||
|
|
||||||
|
$this->withHeader('Authorization', 'Bearer '.$token)
|
||||||
|
->putJson('/api/v1/admin/settings/wallet.transfer_in_max_minor', [
|
||||||
|
'value' => 50,
|
||||||
|
])
|
||||||
|
->assertUnprocessable();
|
||||||
|
|
||||||
|
expect(LotterySetting::query()->where('setting_key', 'wallet.transfer_in_max_minor')->value('value_json'))->toBe(10_000);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('settings updates require permission for their setting group', function (): void {
|
||||||
|
LotterySettings::put('draw.interval_minutes', 5, 'draw');
|
||||||
|
LotterySettings::put('frontend.play_rules_html_zh', '<div>old</div>', 'frontend');
|
||||||
|
|
||||||
|
$token = settingsReadOnlyToken();
|
||||||
|
|
||||||
|
$this->withHeader('Authorization', 'Bearer '.$token)
|
||||||
|
->putJson('/api/v1/admin/settings/draw.interval_minutes', [
|
||||||
|
'value' => 10,
|
||||||
|
])
|
||||||
|
->assertForbidden();
|
||||||
|
|
||||||
|
$this->withHeader('Authorization', 'Bearer '.$token)
|
||||||
|
->putJson('/api/v1/admin/settings/frontend.play_rules_html_zh', [
|
||||||
|
'value' => '<div>new</div>',
|
||||||
|
])
|
||||||
|
->assertOk();
|
||||||
|
|
||||||
|
expect(LotterySetting::query()->where('setting_key', 'draw.interval_minutes')->value('value_json'))->toBe(5)
|
||||||
|
->and(LotterySetting::query()->where('setting_key', 'frontend.play_rules_html_zh')->value('value_json'))->toBe('<div>new</div>');
|
||||||
|
});
|
||||||
|
|||||||
185
tests/Feature/FinancialChainAuditCommandTest.php
Normal file
185
tests/Feature/FinancialChainAuditCommandTest.php
Normal file
@@ -0,0 +1,185 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Models\Player;
|
||||||
|
use App\Models\PlayerWallet;
|
||||||
|
use App\Models\WalletTxn;
|
||||||
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
uses(RefreshDatabase::class);
|
||||||
|
|
||||||
|
beforeEach(function (): void {
|
||||||
|
Schema::create('players', function (Blueprint $table): void {
|
||||||
|
$table->id();
|
||||||
|
$table->string('site_code');
|
||||||
|
$table->string('site_player_id');
|
||||||
|
$table->string('username')->nullable();
|
||||||
|
$table->string('nickname')->nullable();
|
||||||
|
$table->string('default_currency')->default('NPR');
|
||||||
|
$table->string('funding_mode')->default('wallet');
|
||||||
|
$table->smallInteger('status')->default(0);
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
|
||||||
|
Schema::create('player_wallets', function (Blueprint $table): void {
|
||||||
|
$table->id();
|
||||||
|
$table->foreignId('player_id');
|
||||||
|
$table->string('wallet_type');
|
||||||
|
$table->string('currency_code');
|
||||||
|
$table->bigInteger('balance')->default(0);
|
||||||
|
$table->bigInteger('frozen_balance')->default(0);
|
||||||
|
$table->smallInteger('status')->default(0);
|
||||||
|
$table->integer('version')->default(0);
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
|
||||||
|
Schema::create('wallet_txns', function (Blueprint $table): void {
|
||||||
|
$table->id();
|
||||||
|
$table->string('txn_no');
|
||||||
|
$table->foreignId('player_id');
|
||||||
|
$table->foreignId('wallet_id');
|
||||||
|
$table->string('biz_type');
|
||||||
|
$table->string('biz_no')->nullable();
|
||||||
|
$table->smallInteger('direction');
|
||||||
|
$table->bigInteger('amount');
|
||||||
|
$table->bigInteger('balance_before');
|
||||||
|
$table->bigInteger('balance_after');
|
||||||
|
$table->string('status');
|
||||||
|
$table->string('external_ref_no')->nullable();
|
||||||
|
$table->string('idempotent_key')->nullable();
|
||||||
|
$table->string('remark')->nullable();
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
|
||||||
|
Schema::create('transfer_orders', function (Blueprint $table): void {
|
||||||
|
$table->id();
|
||||||
|
$table->string('transfer_no');
|
||||||
|
$table->foreignId('player_id');
|
||||||
|
$table->string('direction');
|
||||||
|
$table->string('currency_code');
|
||||||
|
$table->bigInteger('amount');
|
||||||
|
$table->string('idempotent_key');
|
||||||
|
$table->string('status');
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
|
||||||
|
Schema::create('player_credit_accounts', function (Blueprint $table): void {
|
||||||
|
$table->foreignId('player_id')->primary();
|
||||||
|
$table->bigInteger('credit_limit')->default(0);
|
||||||
|
$table->bigInteger('used_credit')->default(0);
|
||||||
|
$table->bigInteger('frozen_credit')->default(0);
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
|
||||||
|
Schema::create('credit_ledger', function (Blueprint $table): void {
|
||||||
|
$table->id();
|
||||||
|
$table->string('owner_type');
|
||||||
|
$table->unsignedBigInteger('owner_id');
|
||||||
|
$table->bigInteger('amount');
|
||||||
|
$table->string('reason');
|
||||||
|
$table->string('ref_type')->nullable();
|
||||||
|
$table->unsignedBigInteger('ref_id')->nullable();
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
|
||||||
|
Schema::create('ticket_items', function (Blueprint $table): void {
|
||||||
|
$table->id();
|
||||||
|
});
|
||||||
|
|
||||||
|
Schema::create('settlement_bills', function (Blueprint $table): void {
|
||||||
|
$table->id();
|
||||||
|
$table->string('bill_type')->default('player');
|
||||||
|
$table->bigInteger('net_amount')->default(0);
|
||||||
|
$table->bigInteger('paid_amount')->default(0);
|
||||||
|
$table->bigInteger('unpaid_amount')->default(0);
|
||||||
|
$table->json('meta_json')->nullable();
|
||||||
|
});
|
||||||
|
|
||||||
|
Schema::create('payment_records', function (Blueprint $table): void {
|
||||||
|
$table->id();
|
||||||
|
$table->foreignId('settlement_bill_id');
|
||||||
|
$table->bigInteger('amount');
|
||||||
|
$table->string('status');
|
||||||
|
$table->timestamp('confirmed_at')->nullable();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('financial chain audit passes for consistent wallet ledger', function (): void {
|
||||||
|
$player = Player::query()->create([
|
||||||
|
'site_code' => 'main',
|
||||||
|
'site_player_id' => 'financial-audit-ok',
|
||||||
|
'username' => 'financial_audit_ok',
|
||||||
|
'nickname' => null,
|
||||||
|
'default_currency' => 'NPR',
|
||||||
|
'status' => 0,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$wallet = PlayerWallet::query()->create([
|
||||||
|
'player_id' => $player->id,
|
||||||
|
'wallet_type' => 'lottery',
|
||||||
|
'currency_code' => 'NPR',
|
||||||
|
'balance' => 1000,
|
||||||
|
'frozen_balance' => 0,
|
||||||
|
'status' => 0,
|
||||||
|
'version' => 1,
|
||||||
|
]);
|
||||||
|
|
||||||
|
WalletTxn::query()->create([
|
||||||
|
'txn_no' => 'WX_financial_audit_ok',
|
||||||
|
'player_id' => $player->id,
|
||||||
|
'wallet_id' => $wallet->id,
|
||||||
|
'biz_type' => 'manual_seed',
|
||||||
|
'biz_no' => 'manual_seed_ok',
|
||||||
|
'direction' => 1,
|
||||||
|
'amount' => 1000,
|
||||||
|
'balance_before' => 0,
|
||||||
|
'balance_after' => 1000,
|
||||||
|
'status' => 'posted',
|
||||||
|
'idempotent_key' => 'financial-audit-ok',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->artisan('lottery:audit-financial-chain')
|
||||||
|
->expectsOutputToContain('Financial chain audit passed.')
|
||||||
|
->assertExitCode(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('financial chain audit reports wallet balance drift', function (): void {
|
||||||
|
$player = Player::query()->create([
|
||||||
|
'site_code' => 'main',
|
||||||
|
'site_player_id' => 'financial-audit-bad',
|
||||||
|
'username' => 'financial_audit_bad',
|
||||||
|
'nickname' => null,
|
||||||
|
'default_currency' => 'NPR',
|
||||||
|
'status' => 0,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$wallet = PlayerWallet::query()->create([
|
||||||
|
'player_id' => $player->id,
|
||||||
|
'wallet_type' => 'lottery',
|
||||||
|
'currency_code' => 'NPR',
|
||||||
|
'balance' => 900,
|
||||||
|
'frozen_balance' => 0,
|
||||||
|
'status' => 0,
|
||||||
|
'version' => 1,
|
||||||
|
]);
|
||||||
|
|
||||||
|
WalletTxn::query()->create([
|
||||||
|
'txn_no' => 'WX_financial_audit_bad',
|
||||||
|
'player_id' => $player->id,
|
||||||
|
'wallet_id' => $wallet->id,
|
||||||
|
'biz_type' => 'manual_seed',
|
||||||
|
'biz_no' => 'manual_seed_bad',
|
||||||
|
'direction' => 1,
|
||||||
|
'amount' => 1000,
|
||||||
|
'balance_before' => 0,
|
||||||
|
'balance_after' => 1000,
|
||||||
|
'status' => 'posted',
|
||||||
|
'idempotent_key' => 'financial-audit-bad',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->artisan('lottery:audit-financial-chain')
|
||||||
|
->expectsOutputToContain('Financial chain audit found')
|
||||||
|
->expectsOutputToContain('[wallet_latest_mismatch]')
|
||||||
|
->assertExitCode(1);
|
||||||
|
});
|
||||||
@@ -2,7 +2,9 @@
|
|||||||
|
|
||||||
use Tests\TestCase;
|
use Tests\TestCase;
|
||||||
use App\Models\AdminUser;
|
use App\Models\AdminUser;
|
||||||
|
use Illuminate\Support\Carbon;
|
||||||
use Illuminate\Support\Facades\DB;
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -104,3 +106,32 @@ function bindAdminUserToAgent(AdminUser $admin, int $agentNodeId): void
|
|||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function ensureAdminActionCatalogSeeded(): void
|
||||||
|
{
|
||||||
|
if (! Schema::hasTable('admin_action_catalog')) {
|
||||||
|
Schema::create('admin_action_catalog', static function ($table): void {
|
||||||
|
$table->id();
|
||||||
|
$table->string('code')->unique();
|
||||||
|
$table->string('name');
|
||||||
|
$table->unsignedTinyInteger('status')->default(1);
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
$now = Carbon::now();
|
||||||
|
foreach ([
|
||||||
|
['code' => 'view', 'name' => '查看'],
|
||||||
|
['code' => 'manage', 'name' => '管理'],
|
||||||
|
] as $row) {
|
||||||
|
DB::table('admin_action_catalog')->updateOrInsert(
|
||||||
|
['code' => $row['code']],
|
||||||
|
[
|
||||||
|
'name' => $row['name'],
|
||||||
|
'status' => 1,
|
||||||
|
'created_at' => $now,
|
||||||
|
'updated_at' => $now,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user