feat: 增强管理员功能与数据处理
- 在多个控制器中引入 agent_node_id,以支持基于代理节点的权限和数据过滤。 - 更新 AdminRole 和 AdminUser 模型,新增角色范围和代理节点相关功能,提升角色管理的灵活性。 - 在请求验证中添加 agent_node_id 字段,确保 API 接口支持代理节点的相关操作。 - 优化 LotterySettings 服务,支持批量写入设置,提升配置管理的效率。 - 更新仪表板和报告服务,增强数据统计功能,确保管理员能够获取更全面的统计信息。
This commit is contained in:
@@ -45,7 +45,7 @@ final class AdminDashboardAnalyticsBuilder
|
||||
$dateFrom = $range['date_from'];
|
||||
$dateTo = $range['date_to'];
|
||||
|
||||
$trend = $this->reportQuery->dailyProfitSeriesFilled($dateFrom, $dateTo);
|
||||
$trend = $this->reportQuery->dailyProfitSeriesFilled($dateFrom, $dateTo, scopedAdmin: $admin);
|
||||
|
||||
return [
|
||||
'period' => $period,
|
||||
@@ -53,8 +53,8 @@ final class AdminDashboardAnalyticsBuilder
|
||||
'play_code' => $playCode,
|
||||
'date_from' => $dateFrom,
|
||||
'date_to' => $dateTo,
|
||||
'currency_code' => $this->reportQuery->resolvePeriodCurrencyCode($dateFrom, $dateTo),
|
||||
'summary' => $this->reportQuery->periodFinanceTotals($dateFrom, $dateTo),
|
||||
'currency_code' => $this->reportQuery->resolvePeriodCurrencyCode($dateFrom, $dateTo, $admin),
|
||||
'summary' => $this->reportQuery->periodFinanceTotals($dateFrom, $dateTo, $admin),
|
||||
'daily_series' => $trend['series'],
|
||||
'chart_meta' => [
|
||||
'chart_date_from' => $trend['chart_date_from'],
|
||||
@@ -66,6 +66,14 @@ final class AdminDashboardAnalyticsBuilder
|
||||
$dateFrom,
|
||||
$dateTo,
|
||||
$playCode,
|
||||
scopedAdmin: $admin,
|
||||
),
|
||||
'agent_breakdown' => $this->reportQuery->agentRankingRows(
|
||||
$dateFrom,
|
||||
$dateTo,
|
||||
$playCode,
|
||||
limit: 200,
|
||||
scopedAdmin: $admin,
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ use App\Models\SettlementBatch;
|
||||
use App\Models\DrawResultBatch;
|
||||
use App\Lottery\DrawResultBatchStatus;
|
||||
use App\Services\Draw\DrawHallSnapshotBuilder;
|
||||
use App\Support\AdminDataScope;
|
||||
|
||||
/**
|
||||
* 后台首页仪表盘:聚合大厅快照、当期财务、期号面板、风控摘要、异常转账计数。
|
||||
@@ -52,11 +53,11 @@ final class AdminDashboardSnapshotBuilder
|
||||
];
|
||||
|
||||
if ($canDraw) {
|
||||
$this->fillPlatformOverview($out);
|
||||
$this->fillPlatformOverview($out, $admin);
|
||||
}
|
||||
|
||||
if ($canWallet) {
|
||||
$out['abnormal_transfer_total'] = $this->abnormalTransferTotal();
|
||||
$out['abnormal_transfer_total'] = $this->abnormalTransferTotal($admin);
|
||||
}
|
||||
|
||||
if ($hall === null) {
|
||||
@@ -81,7 +82,7 @@ final class AdminDashboardSnapshotBuilder
|
||||
];
|
||||
|
||||
if ($canDraw) {
|
||||
$out['finance'] = $this->financeSummary($draw);
|
||||
$out['finance'] = $this->financeSummary($draw, $admin);
|
||||
$out['draw'] = $this->drawPanel($draw);
|
||||
$out['risk'] = $this->riskPanel($draw);
|
||||
}
|
||||
@@ -90,10 +91,10 @@ final class AdminDashboardSnapshotBuilder
|
||||
}
|
||||
|
||||
/** @param array<string, mixed> $out */
|
||||
private function fillPlatformOverview(array &$out): void
|
||||
private function fillPlatformOverview(array &$out, AdminUser $admin): void
|
||||
{
|
||||
$out['today_finance'] = $this->todayFinanceSummary();
|
||||
$out['lifetime_finance'] = $this->reportQuery->platformLifetimeTotals();
|
||||
$out['today_finance'] = $this->todayFinanceSummary($admin);
|
||||
$out['lifetime_finance'] = $this->reportQuery->platformLifetimeTotals($admin);
|
||||
$out['platform_risk'] = $this->platformRiskSummary();
|
||||
$out['result_batch_queue'] = $this->resultBatchQueue();
|
||||
}
|
||||
@@ -114,11 +115,13 @@ final class AdminDashboardSnapshotBuilder
|
||||
|| $admin->hasAdminPermission('prd.wallet_reconcile.view_cs');
|
||||
}
|
||||
|
||||
private function abnormalTransferTotal(): int
|
||||
private function abnormalTransferTotal(AdminUser $admin): int
|
||||
{
|
||||
return (int) TransferOrder::query()
|
||||
->whereIn('status', ['processing', 'failed', 'pending_reconcile'])
|
||||
->count();
|
||||
$query = TransferOrder::query()
|
||||
->whereIn('status', ['processing', 'failed', 'pending_reconcile']);
|
||||
AdminDataScope::applyEloquentViaPlayer($query, $admin);
|
||||
|
||||
return (int) $query->count();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -126,10 +129,10 @@ final class AdminDashboardSnapshotBuilder
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function todayFinanceSummary(): array
|
||||
private function todayFinanceSummary(AdminUser $admin): array
|
||||
{
|
||||
$today = now()->toDateString();
|
||||
$rows = $this->reportQuery->dailyProfitRows($today, $today);
|
||||
$rows = $this->reportQuery->dailyProfitRows($today, $today, $admin);
|
||||
$row = $rows[0] ?? [
|
||||
'business_date' => $today,
|
||||
'total_bet_minor' => 0,
|
||||
@@ -152,20 +155,23 @@ final class AdminDashboardSnapshotBuilder
|
||||
}
|
||||
|
||||
/** @return array<string, mixed> */
|
||||
private function financeSummary(Draw $draw): array
|
||||
private function financeSummary(Draw $draw, AdminUser $admin): array
|
||||
{
|
||||
$drawId = (int) $draw->id;
|
||||
|
||||
$totalBetMinor = (int) TicketOrder::query()->where('draw_id', $drawId)->sum('total_actual_deduct');
|
||||
$orderCount = (int) TicketOrder::query()->where('draw_id', $drawId)->count();
|
||||
$itemCount = (int) TicketItem::query()->where('draw_id', $drawId)->count();
|
||||
$orderQuery = TicketOrder::query()->where('draw_id', $drawId);
|
||||
$itemQuery = TicketItem::query()->where('draw_id', $drawId);
|
||||
AdminDataScope::applyEloquentViaPlayer($orderQuery, $admin);
|
||||
AdminDataScope::applyEloquentViaPlayer($itemQuery, $admin);
|
||||
|
||||
$currencyCode = (string) (TicketOrder::query()
|
||||
->where('draw_id', $drawId)
|
||||
->value('currency_code') ?? '');
|
||||
$totalBetMinor = (int) $orderQuery->sum('total_actual_deduct');
|
||||
$orderCount = (int) $orderQuery->count();
|
||||
$itemCount = (int) $itemQuery->count();
|
||||
|
||||
$totalWinMinor = (int) TicketItem::query()->where('draw_id', $drawId)->sum('win_amount');
|
||||
$totalJackpotWinMinor = (int) TicketItem::query()->where('draw_id', $drawId)->sum('jackpot_win_amount');
|
||||
$currencyCode = (string) ((clone $orderQuery)->value('currency_code') ?? '');
|
||||
|
||||
$totalWinMinor = (int) $itemQuery->sum('win_amount');
|
||||
$totalJackpotWinMinor = (int) (clone $itemQuery)->sum('jackpot_win_amount');
|
||||
$totalPayoutMinor = $totalWinMinor + $totalJackpotWinMinor;
|
||||
$approxHouseGrossMinor = $totalBetMinor - $totalPayoutMinor;
|
||||
|
||||
|
||||
@@ -68,9 +68,9 @@ final class AdminReportJobService
|
||||
/**
|
||||
* @return list<array<int, string|int|float|null>>
|
||||
*/
|
||||
public function reportRows(string $reportType, ?array $filterJson): array
|
||||
public function reportRows(string $reportType, ?array $filterJson, ?AdminUser $scopedAdmin = null): array
|
||||
{
|
||||
return $this->queryService->reportRows($reportType, $filterJson);
|
||||
return $this->queryService->reportRows($reportType, $filterJson, $scopedAdmin);
|
||||
}
|
||||
|
||||
public function reportLabel(string $reportType): string
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
|
||||
namespace App\Services\Admin;
|
||||
|
||||
use App\Models\AdminUser;
|
||||
use App\Models\AuditLog;
|
||||
use App\Support\AdminDataScope;
|
||||
use App\Models\Draw;
|
||||
use App\Models\RiskPool;
|
||||
use App\Models\RiskPoolLockLog;
|
||||
@@ -109,9 +111,9 @@ final class AdminReportQueryService
|
||||
* business_day_count: int
|
||||
* }
|
||||
*/
|
||||
public function periodFinanceTotals(string $dateFrom, string $dateTo): array
|
||||
public function periodFinanceTotals(string $dateFrom, string $dateTo, ?AdminUser $scopedAdmin = null): array
|
||||
{
|
||||
$rows = $this->dailyProfitRows($dateFrom, $dateTo);
|
||||
$rows = $this->dailyProfitRows($dateFrom, $dateTo, $scopedAdmin);
|
||||
$totalBet = 0;
|
||||
$totalPayout = 0;
|
||||
$totalGross = 0;
|
||||
@@ -121,9 +123,11 @@ final class AdminReportQueryService
|
||||
$totalGross += (int) $row['approx_house_gross_minor'];
|
||||
}
|
||||
|
||||
$activity = DB::table('draws as d')
|
||||
$activityQuery = DB::table('draws as d')
|
||||
->join('ticket_orders as o', 'o.draw_id', '=', 'd.id')
|
||||
->whereBetween('d.business_date', [$dateFrom, $dateTo])
|
||||
->whereBetween('d.business_date', [$dateFrom, $dateTo]);
|
||||
AdminDataScope::applyToTicketOrdersViaPlayer($activityQuery, $scopedAdmin, 'o');
|
||||
$activity = $activityQuery
|
||||
->selectRaw('COUNT(DISTINCT d.id) as draw_count')
|
||||
->selectRaw('COUNT(DISTINCT d.business_date) as business_day_count')
|
||||
->first();
|
||||
@@ -142,7 +146,7 @@ final class AdminReportQueryService
|
||||
*
|
||||
* @return list<array<string, mixed>>
|
||||
*/
|
||||
public function dailyProfitSeriesFilled(string $dateFrom, string $dateTo, int $maxDays = 90): array
|
||||
public function dailyProfitSeriesFilled(string $dateFrom, string $dateTo, int $maxDays = 90, ?AdminUser $scopedAdmin = null): array
|
||||
{
|
||||
$from = Carbon::parse($dateFrom)->startOfDay();
|
||||
$to = Carbon::parse($dateTo)->startOfDay();
|
||||
@@ -156,7 +160,7 @@ final class AdminReportQueryService
|
||||
$truncated = true;
|
||||
}
|
||||
|
||||
$indexed = collect($this->dailyProfitRows($chartFrom, $chartTo))->keyBy('business_date');
|
||||
$indexed = collect($this->dailyProfitRows($chartFrom, $chartTo, $scopedAdmin))->keyBy('business_date');
|
||||
$cursor = Carbon::parse($chartFrom)->startOfDay();
|
||||
$end = Carbon::parse($chartTo)->startOfDay();
|
||||
$series = [];
|
||||
@@ -189,8 +193,9 @@ final class AdminReportQueryService
|
||||
string $dateTo,
|
||||
?string $playCode = null,
|
||||
int $limit = 12,
|
||||
?AdminUser $scopedAdmin = null,
|
||||
): array {
|
||||
return $this->playDimensionBaseQuery($playCode, $dateFrom, $dateTo)
|
||||
return $this->playDimensionBaseQuery($playCode, $dateFrom, $dateTo, $scopedAdmin)
|
||||
->orderByDesc('total_bet_minor')
|
||||
->limit($limit)
|
||||
->get()
|
||||
@@ -207,20 +212,78 @@ final class AdminReportQueryService
|
||||
->all();
|
||||
}
|
||||
|
||||
public function resolvePeriodCurrencyCode(string $dateFrom, string $dateTo): ?string
|
||||
/**
|
||||
* 仪表盘「代理排行」:按代理子树聚合投注/派彩/盈亏(与 daily-profit 同口径)。
|
||||
*
|
||||
* @return list<array{
|
||||
* agent_node_id: int,
|
||||
* agent_code: string,
|
||||
* agent_name: string,
|
||||
* total_bet_minor: int,
|
||||
* total_payout_minor: int,
|
||||
* approx_house_gross_minor: int
|
||||
* }>
|
||||
*/
|
||||
public function agentRankingRows(
|
||||
string $dateFrom,
|
||||
string $dateTo,
|
||||
?string $playCode = null,
|
||||
int $limit = 200,
|
||||
?AdminUser $scopedAdmin = null,
|
||||
): array {
|
||||
$query = DB::table('ticket_items as ti')
|
||||
->join('ticket_orders as o', 'o.id', '=', 'ti.order_id')
|
||||
->leftJoin('players as p', 'p.id', '=', 'ti.player_id')
|
||||
->leftJoin('agent_nodes as an', 'an.id', '=', 'p.agent_node_id')
|
||||
->selectRaw('p.agent_node_id as agent_node_id')
|
||||
->selectRaw('an.code as agent_code')
|
||||
->selectRaw('an.name as agent_name')
|
||||
->selectRaw('SUM(ti.actual_deduct_amount) as total_bet_minor')
|
||||
->selectRaw('SUM(ti.win_amount + ti.jackpot_win_amount) as total_payout_minor')
|
||||
->selectRaw('SUM(ti.actual_deduct_amount) - SUM(ti.win_amount + ti.jackpot_win_amount) as approx_house_gross_minor')
|
||||
->whereDate('o.created_at', '>=', $dateFrom)
|
||||
->whereDate('o.created_at', '<=', $dateTo)
|
||||
->whereNotNull('p.agent_node_id')
|
||||
->groupBy('p.agent_node_id', 'an.code', 'an.name');
|
||||
|
||||
if ($playCode !== null && $playCode !== '') {
|
||||
$query->where('ti.play_code', $playCode);
|
||||
}
|
||||
|
||||
AdminDataScope::applyToPlayersAlias($query, $scopedAdmin, 'p');
|
||||
|
||||
return $query
|
||||
->orderByDesc('total_bet_minor')
|
||||
->limit($limit)
|
||||
->get()
|
||||
->map(static function (object $row): array {
|
||||
return [
|
||||
'agent_node_id' => (int) $row->agent_node_id,
|
||||
'agent_code' => (string) ($row->agent_code ?? ''),
|
||||
'agent_name' => (string) ($row->agent_name ?? ''),
|
||||
'total_bet_minor' => (int) $row->total_bet_minor,
|
||||
'total_payout_minor' => (int) $row->total_payout_minor,
|
||||
'approx_house_gross_minor' => (int) $row->approx_house_gross_minor,
|
||||
];
|
||||
})
|
||||
->values()
|
||||
->all();
|
||||
}
|
||||
|
||||
public function resolvePeriodCurrencyCode(string $dateFrom, string $dateTo, ?AdminUser $scopedAdmin = null): ?string
|
||||
{
|
||||
$currencyCode = (string) (DB::table('ticket_orders as o')
|
||||
$currencyQuery = DB::table('ticket_orders as o')
|
||||
->join('draws as d', 'd.id', '=', 'o.draw_id')
|
||||
->whereBetween('d.business_date', [$dateFrom, $dateTo])
|
||||
->orderByDesc('o.id')
|
||||
->value('o.currency_code') ?? '');
|
||||
->whereBetween('d.business_date', [$dateFrom, $dateTo]);
|
||||
AdminDataScope::applyToTicketOrdersViaPlayer($currencyQuery, $scopedAdmin, 'o');
|
||||
$currencyCode = (string) ($currencyQuery->orderByDesc('o.id')->value('o.currency_code') ?? '');
|
||||
|
||||
return $currencyCode !== '' ? $currencyCode : null;
|
||||
}
|
||||
|
||||
public function dailyProfitPaginated(string $dateFrom, string $dateTo, int $page, int $perPage): LengthAwarePaginator
|
||||
public function dailyProfitPaginated(string $dateFrom, string $dateTo, int $page, int $perPage, ?AdminUser $scopedAdmin = null): LengthAwarePaginator
|
||||
{
|
||||
$rows = $this->dailyProfitRows($dateFrom, $dateTo);
|
||||
$rows = $this->dailyProfitRows($dateFrom, $dateTo, $scopedAdmin);
|
||||
$total = count($rows);
|
||||
$offset = max(0, ($page - 1) * $perPage);
|
||||
$items = array_slice($rows, $offset, $perPage);
|
||||
@@ -233,14 +296,18 @@ final class AdminReportQueryService
|
||||
/**
|
||||
* @return list<array<string, mixed>>
|
||||
*/
|
||||
public function dailyProfitRows(string $dateFrom, string $dateTo): array
|
||||
public function dailyProfitRows(string $dateFrom, string $dateTo, ?AdminUser $scopedAdmin = null): array
|
||||
{
|
||||
$betSub = DB::table('ticket_orders')
|
||||
->selectRaw('draw_id, SUM(total_actual_deduct) as total_bet_minor')
|
||||
->groupBy('draw_id');
|
||||
$payoutSub = DB::table('ticket_items')
|
||||
->selectRaw('draw_id, SUM(win_amount + jackpot_win_amount) as total_payout_minor')
|
||||
->groupBy('draw_id');
|
||||
$betSub = DB::table('ticket_orders as o')
|
||||
->selectRaw('o.draw_id, SUM(o.total_actual_deduct) as total_bet_minor')
|
||||
->groupBy('o.draw_id');
|
||||
AdminDataScope::applyToTicketOrdersViaPlayer($betSub, $scopedAdmin, 'o');
|
||||
|
||||
$payoutSub = DB::table('ticket_items as ti')
|
||||
->join('ticket_orders as o', 'o.id', '=', 'ti.order_id')
|
||||
->selectRaw('ti.draw_id, SUM(ti.win_amount + ti.jackpot_win_amount) as total_payout_minor')
|
||||
->groupBy('ti.draw_id');
|
||||
AdminDataScope::applyToTicketOrdersViaPlayer($payoutSub, $scopedAdmin, 'o');
|
||||
|
||||
return DB::table('draws as d')
|
||||
->whereBetween('d.business_date', [$dateFrom, $dateTo])
|
||||
@@ -284,19 +351,26 @@ final class AdminReportQueryService
|
||||
* date_to: ?string
|
||||
* }
|
||||
*/
|
||||
public function platformLifetimeTotals(): array
|
||||
public function platformLifetimeTotals(?AdminUser $scopedAdmin = null): array
|
||||
{
|
||||
$totalBetMinor = (int) DB::table('ticket_orders')->sum('total_actual_deduct');
|
||||
$betQuery = DB::table('ticket_orders as o');
|
||||
AdminDataScope::applyToTicketOrdersViaPlayer($betQuery, $scopedAdmin, 'o');
|
||||
$totalBetMinor = (int) $betQuery->sum('o.total_actual_deduct');
|
||||
|
||||
$payoutAgg = DB::table('ticket_items')
|
||||
->selectRaw('COALESCE(SUM(win_amount), 0) as win_minor, COALESCE(SUM(jackpot_win_amount), 0) as jackpot_minor')
|
||||
$payoutQuery = DB::table('ticket_items as ti')
|
||||
->join('ticket_orders as o', 'o.id', '=', 'ti.order_id');
|
||||
AdminDataScope::applyToTicketOrdersViaPlayer($payoutQuery, $scopedAdmin, 'o');
|
||||
$payoutAgg = $payoutQuery
|
||||
->selectRaw('COALESCE(SUM(ti.win_amount), 0) as win_minor, COALESCE(SUM(ti.jackpot_win_amount), 0) as jackpot_minor')
|
||||
->first();
|
||||
$totalWinMinor = (int) ($payoutAgg->win_minor ?? 0);
|
||||
$totalJackpotMinor = (int) ($payoutAgg->jackpot_minor ?? 0);
|
||||
$totalPayoutMinor = $totalWinMinor + $totalJackpotMinor;
|
||||
|
||||
$activity = DB::table('draws as d')
|
||||
->join('ticket_orders as o', 'o.draw_id', '=', 'd.id')
|
||||
$activityQuery = DB::table('draws as d')
|
||||
->join('ticket_orders as o', 'o.draw_id', '=', 'd.id');
|
||||
AdminDataScope::applyToTicketOrdersViaPlayer($activityQuery, $scopedAdmin, 'o');
|
||||
$activity = $activityQuery
|
||||
->selectRaw('COUNT(DISTINCT d.id) as draw_count')
|
||||
->selectRaw('COUNT(DISTINCT d.business_date) as business_day_count')
|
||||
->selectRaw('MIN(d.business_date) as date_from')
|
||||
@@ -309,7 +383,15 @@ final class AdminReportQueryService
|
||||
$dateFrom = $this->formatBusinessDateValue($activity?->date_from);
|
||||
$dateTo = $this->formatBusinessDateValue($activity?->date_to);
|
||||
|
||||
$currencyCode = (string) (DB::table('ticket_orders')->orderByDesc('id')->value('currency_code') ?? '');
|
||||
$currencyQuery = DB::table('ticket_orders as o');
|
||||
AdminDataScope::applyToTicketOrdersViaPlayer($currencyQuery, $scopedAdmin, 'o');
|
||||
$currencyCode = (string) ($currencyQuery->orderByDesc('o.id')->value('o.currency_code') ?? '');
|
||||
|
||||
$orderCountQuery = DB::table('ticket_orders as o');
|
||||
AdminDataScope::applyToTicketOrdersViaPlayer($orderCountQuery, $scopedAdmin, 'o');
|
||||
$itemCountQuery = DB::table('ticket_items as ti')
|
||||
->join('ticket_orders as o', 'o.id', '=', 'ti.order_id');
|
||||
AdminDataScope::applyToTicketOrdersViaPlayer($itemCountQuery, $scopedAdmin, 'o');
|
||||
|
||||
return [
|
||||
'currency_code' => $currencyCode !== '' ? $currencyCode : null,
|
||||
@@ -318,8 +400,8 @@ final class AdminReportQueryService
|
||||
'total_jackpot_minor' => $totalJackpotMinor,
|
||||
'total_payout_minor' => $totalPayoutMinor,
|
||||
'approx_house_gross_minor' => $totalBetMinor - $totalPayoutMinor,
|
||||
'order_count' => (int) DB::table('ticket_orders')->count(),
|
||||
'ticket_item_count' => (int) DB::table('ticket_items')->count(),
|
||||
'order_count' => (int) $orderCountQuery->count(),
|
||||
'ticket_item_count' => (int) $itemCountQuery->count('ti.id'),
|
||||
'draw_count' => $drawCount,
|
||||
'business_day_count' => $businessDayCount,
|
||||
'date_from' => $dateFrom,
|
||||
@@ -333,8 +415,10 @@ final class AdminReportQueryService
|
||||
string $dateTo,
|
||||
int $page,
|
||||
int $perPage,
|
||||
?AdminUser $scopedAdmin = null,
|
||||
?int $requestedAgentNodeId = null,
|
||||
): LengthAwarePaginator {
|
||||
$query = $this->playerWinLossBaseQuery($playerId, $dateFrom, $dateTo);
|
||||
$query = $this->playerWinLossBaseQuery($playerId, $dateFrom, $dateTo, $scopedAdmin, $requestedAgentNodeId);
|
||||
|
||||
return $query->paginate($perPage, ['*'], 'page', $page);
|
||||
}
|
||||
@@ -345,8 +429,9 @@ final class AdminReportQueryService
|
||||
string $dateTo,
|
||||
int $page,
|
||||
int $perPage,
|
||||
?AdminUser $scopedAdmin = null,
|
||||
): LengthAwarePaginator {
|
||||
$query = $this->playDimensionBaseQuery($playCode, $dateFrom, $dateTo);
|
||||
$query = $this->playDimensionBaseQuery($playCode, $dateFrom, $dateTo, $scopedAdmin);
|
||||
|
||||
return $query->paginate($perPage, ['*'], 'page', $page);
|
||||
}
|
||||
@@ -357,8 +442,9 @@ final class AdminReportQueryService
|
||||
string $dateTo,
|
||||
int $page,
|
||||
int $perPage,
|
||||
?AdminUser $scopedAdmin = null,
|
||||
): LengthAwarePaginator {
|
||||
$query = $this->rebateCommissionBaseQuery($playCode, $dateFrom, $dateTo);
|
||||
$query = $this->rebateCommissionBaseQuery($playCode, $dateFrom, $dateTo, $scopedAdmin);
|
||||
|
||||
return $query->paginate($perPage, ['*'], 'page', $page);
|
||||
}
|
||||
@@ -366,21 +452,21 @@ final class AdminReportQueryService
|
||||
/**
|
||||
* @return list<array<int, string|int|float|null>>
|
||||
*/
|
||||
public function reportRows(string $reportType, ?array $filterJson): array
|
||||
public function reportRows(string $reportType, ?array $filterJson, ?AdminUser $scopedAdmin = null): array
|
||||
{
|
||||
$range = $this->resolveDateRange($filterJson);
|
||||
$dateFrom = $range['date_from'];
|
||||
$dateTo = $range['date_to'];
|
||||
|
||||
return match ($reportType) {
|
||||
'draw_profit_summary' => $this->drawProfitExportRows($filterJson),
|
||||
'daily_profit_summary' => $this->dailyProfitExportRows($dateFrom, $dateTo),
|
||||
'player_win_loss' => $this->playerWinLossExportRows($filterJson, $dateFrom, $dateTo),
|
||||
'play_dimension_report' => $this->playDimensionExportRows($filterJson, $dateFrom, $dateTo),
|
||||
'rebate_commission_report' => $this->rebateCommissionExportRows($filterJson, $dateFrom, $dateTo),
|
||||
'draw_profit_summary' => $this->drawProfitExportRows($filterJson, $scopedAdmin),
|
||||
'daily_profit_summary' => $this->dailyProfitExportRows($dateFrom, $dateTo, $scopedAdmin),
|
||||
'player_win_loss' => $this->playerWinLossExportRows($filterJson, $dateFrom, $dateTo, $scopedAdmin),
|
||||
'play_dimension_report' => $this->playDimensionExportRows($filterJson, $dateFrom, $dateTo, $scopedAdmin),
|
||||
'rebate_commission_report' => $this->rebateCommissionExportRows($filterJson, $dateFrom, $dateTo, $scopedAdmin),
|
||||
'audit_operation_report' => $this->auditExportRows($filterJson, $dateFrom, $dateTo),
|
||||
'wallet_transfer_report', 'transfer_orders_daily' => $this->transferOrdersExportRows($filterJson, $dateFrom, $dateTo),
|
||||
'wallet_txns_daily' => $this->walletTxnsExportRows($filterJson, $dateFrom, $dateTo),
|
||||
'wallet_transfer_report', 'transfer_orders_daily' => $this->transferOrdersExportRows($filterJson, $dateFrom, $dateTo, $scopedAdmin),
|
||||
'wallet_txns_daily' => $this->walletTxnsExportRows($filterJson, $dateFrom, $dateTo, $scopedAdmin),
|
||||
'hot_number_risk_report' => $this->hotNumberRiskExportRows($filterJson),
|
||||
'sold_out_number_report' => $this->soldOutNumberExportRows($filterJson),
|
||||
default => [
|
||||
@@ -427,12 +513,12 @@ final class AdminReportQueryService
|
||||
/**
|
||||
* @return list<array<int, string|int|float|null>>
|
||||
*/
|
||||
private function dailyProfitExportRows(string $dateFrom, string $dateTo): array
|
||||
private function dailyProfitExportRows(string $dateFrom, string $dateTo, ?AdminUser $scopedAdmin = null): array
|
||||
{
|
||||
$rows = [
|
||||
['日期', '下注', '派彩', '盈亏'],
|
||||
];
|
||||
foreach ($this->dailyProfitRows($dateFrom, $dateTo) as $row) {
|
||||
foreach ($this->dailyProfitRows($dateFrom, $dateTo, $scopedAdmin) as $row) {
|
||||
$rows[] = [
|
||||
$row['business_date'],
|
||||
$row['total_bet_minor'],
|
||||
@@ -447,13 +533,13 @@ final class AdminReportQueryService
|
||||
/**
|
||||
* @return list<array<int, string|int|float|null>>
|
||||
*/
|
||||
private function playerWinLossExportRows(?array $filterJson, string $dateFrom, string $dateTo): array
|
||||
private function playerWinLossExportRows(?array $filterJson, string $dateFrom, string $dateTo, ?AdminUser $scopedAdmin = null): array
|
||||
{
|
||||
$playerId = isset($filterJson['player_id']) ? (int) $filterJson['player_id'] : null;
|
||||
$rows = [
|
||||
['玩家ID', '用户名', '下注', '派彩', '净输赢'],
|
||||
];
|
||||
$items = $this->playerWinLossBaseQuery($playerId, $dateFrom, $dateTo)->get();
|
||||
$items = $this->playerWinLossBaseQuery($playerId, $dateFrom, $dateTo, $scopedAdmin)->get();
|
||||
foreach ($items as $row) {
|
||||
$rows[] = [
|
||||
(int) $row->player_id,
|
||||
@@ -470,13 +556,13 @@ final class AdminReportQueryService
|
||||
/**
|
||||
* @return list<array<int, string|int|float|null>>
|
||||
*/
|
||||
private function playDimensionExportRows(?array $filterJson, string $dateFrom, string $dateTo): array
|
||||
private function playDimensionExportRows(?array $filterJson, string $dateFrom, string $dateTo, ?AdminUser $scopedAdmin = null): array
|
||||
{
|
||||
$playCode = isset($filterJson['play_code']) ? trim((string) $filterJson['play_code']) : null;
|
||||
$rows = [
|
||||
['玩法', '维度', '下注', '派彩', '盈亏'],
|
||||
];
|
||||
$items = $this->playDimensionBaseQuery($playCode !== '' ? $playCode : null, $dateFrom, $dateTo)->get();
|
||||
$items = $this->playDimensionBaseQuery($playCode !== '' ? $playCode : null, $dateFrom, $dateTo, $scopedAdmin)->get();
|
||||
foreach ($items as $row) {
|
||||
$rows[] = [
|
||||
(string) $row->play_code,
|
||||
@@ -493,13 +579,13 @@ final class AdminReportQueryService
|
||||
/**
|
||||
* @return list<array<int, string|int|float|null>>
|
||||
*/
|
||||
private function rebateCommissionExportRows(?array $filterJson, string $dateFrom, string $dateTo): array
|
||||
private function rebateCommissionExportRows(?array $filterJson, string $dateFrom, string $dateTo, ?AdminUser $scopedAdmin = null): array
|
||||
{
|
||||
$playCode = isset($filterJson['play_code']) ? trim((string) $filterJson['play_code']) : null;
|
||||
$rows = [
|
||||
['玩法', '回水', '订单数', '注单数'],
|
||||
];
|
||||
$items = $this->rebateCommissionBaseQuery($playCode !== '' ? $playCode : null, $dateFrom, $dateTo)->get();
|
||||
$items = $this->rebateCommissionBaseQuery($playCode !== '' ? $playCode : null, $dateFrom, $dateTo, $scopedAdmin)->get();
|
||||
foreach ($items as $row) {
|
||||
$rows[] = [
|
||||
(string) $row->play_code,
|
||||
@@ -545,30 +631,43 @@ final class AdminReportQueryService
|
||||
}
|
||||
|
||||
/** @return \Illuminate\Database\Query\Builder */
|
||||
private function playerWinLossBaseQuery(?int $playerId, string $dateFrom, string $dateTo)
|
||||
{
|
||||
private function playerWinLossBaseQuery(
|
||||
?int $playerId,
|
||||
string $dateFrom,
|
||||
string $dateTo,
|
||||
?AdminUser $scopedAdmin = null,
|
||||
?int $requestedAgentNodeId = null,
|
||||
) {
|
||||
$query = DB::table('ticket_items as ti')
|
||||
->join('ticket_orders as o', 'o.id', '=', 'ti.order_id')
|
||||
->leftJoin('players as p', 'p.id', '=', 'ti.player_id')
|
||||
->leftJoin('agent_nodes as an', 'an.id', '=', 'p.agent_node_id')
|
||||
->selectRaw('ti.player_id')
|
||||
->selectRaw('p.username as username')
|
||||
->selectRaw('p.agent_node_id as agent_node_id')
|
||||
->selectRaw('an.code as agent_code')
|
||||
->selectRaw('an.name as agent_name')
|
||||
->selectRaw('SUM(ti.actual_deduct_amount) as total_bet_minor')
|
||||
->selectRaw('SUM(ti.win_amount + ti.jackpot_win_amount) as total_payout_minor')
|
||||
->selectRaw('SUM(ti.actual_deduct_amount) - SUM(ti.win_amount + ti.jackpot_win_amount) as net_win_loss_minor')
|
||||
->whereDate('o.created_at', '>=', $dateFrom)
|
||||
->whereDate('o.created_at', '<=', $dateTo)
|
||||
->groupBy('ti.player_id', 'p.username')
|
||||
->groupBy('ti.player_id', 'p.username', 'p.agent_node_id', 'an.code', 'an.name')
|
||||
->orderByDesc('net_win_loss_minor');
|
||||
|
||||
if ($playerId !== null && $playerId > 0) {
|
||||
$query->where('ti.player_id', $playerId);
|
||||
}
|
||||
|
||||
if ($scopedAdmin !== null) {
|
||||
AdminDataScope::applyToPlayersAlias($query, $scopedAdmin, 'p', $requestedAgentNodeId);
|
||||
}
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
/** @return \Illuminate\Database\Query\Builder */
|
||||
private function playDimensionBaseQuery(?string $playCode, string $dateFrom, string $dateTo)
|
||||
private function playDimensionBaseQuery(?string $playCode, string $dateFrom, string $dateTo, ?AdminUser $scopedAdmin = null)
|
||||
{
|
||||
$query = DB::table('ticket_items as ti')
|
||||
->join('ticket_orders as o', 'o.id', '=', 'ti.order_id')
|
||||
@@ -587,11 +686,13 @@ final class AdminReportQueryService
|
||||
$query->where('ti.play_code', $playCode);
|
||||
}
|
||||
|
||||
AdminDataScope::applyToTicketOrdersViaPlayer($query, $scopedAdmin, 'o');
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
/** @return \Illuminate\Database\Query\Builder */
|
||||
private function rebateCommissionBaseQuery(?string $playCode, string $dateFrom, string $dateTo)
|
||||
private function rebateCommissionBaseQuery(?string $playCode, string $dateFrom, string $dateTo, ?AdminUser $scopedAdmin = null)
|
||||
{
|
||||
$query = DB::table('ticket_items as ti')
|
||||
->join('ticket_orders as o', 'o.id', '=', 'ti.order_id')
|
||||
@@ -608,13 +709,15 @@ final class AdminReportQueryService
|
||||
$query->where('ti.play_code', $playCode);
|
||||
}
|
||||
|
||||
AdminDataScope::applyToTicketOrdersViaPlayer($query, $scopedAdmin, 'o');
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<array<int, string|int|float|null>>
|
||||
*/
|
||||
private function drawProfitExportRows(?array $filterJson): array
|
||||
private function drawProfitExportRows(?array $filterJson, ?AdminUser $scopedAdmin = null): array
|
||||
{
|
||||
$draw = $this->resolveDrawForReport($filterJson);
|
||||
if ($draw === null) {
|
||||
@@ -622,12 +725,19 @@ final class AdminReportQueryService
|
||||
}
|
||||
|
||||
$drawId = (int) $draw->id;
|
||||
$totalBetMinor = (int) TicketOrder::query()->where('draw_id', $drawId)->sum('total_actual_deduct');
|
||||
$orderCount = (int) TicketOrder::query()->where('draw_id', $drawId)->count();
|
||||
$itemCount = (int) TicketItem::query()->where('draw_id', $drawId)->count();
|
||||
$currencyCode = (string) (TicketOrder::query()->where('draw_id', $drawId)->value('currency_code') ?? '');
|
||||
$totalWinMinor = (int) TicketItem::query()->where('draw_id', $drawId)->sum('win_amount');
|
||||
$totalJackpotWinMinor = (int) TicketItem::query()->where('draw_id', $drawId)->sum('jackpot_win_amount');
|
||||
$orderQuery = TicketOrder::query()->where('draw_id', $drawId);
|
||||
$itemQuery = TicketItem::query()->where('draw_id', $drawId);
|
||||
if ($scopedAdmin !== null) {
|
||||
AdminDataScope::applyEloquentViaPlayer($orderQuery, $scopedAdmin);
|
||||
AdminDataScope::applyEloquentViaPlayer($itemQuery, $scopedAdmin);
|
||||
}
|
||||
|
||||
$totalBetMinor = (int) $orderQuery->sum('total_actual_deduct');
|
||||
$orderCount = (int) $orderQuery->count();
|
||||
$itemCount = (int) $itemQuery->count();
|
||||
$currencyCode = (string) ((clone $orderQuery)->value('currency_code') ?? '');
|
||||
$totalWinMinor = (int) $itemQuery->sum('win_amount');
|
||||
$totalJackpotWinMinor = (int) (clone $itemQuery)->sum('jackpot_win_amount');
|
||||
$totalPayoutMinor = $totalWinMinor + $totalJackpotWinMinor;
|
||||
$approxHouseGrossMinor = $totalBetMinor - $totalPayoutMinor;
|
||||
|
||||
@@ -842,7 +952,7 @@ final class AdminReportQueryService
|
||||
/**
|
||||
* @return list<array<int, string|int|float|null>>
|
||||
*/
|
||||
private function transferOrdersExportRows(?array $filterJson, string $dateFrom, string $dateTo): array
|
||||
private function transferOrdersExportRows(?array $filterJson, string $dateFrom, string $dateTo, ?AdminUser $scopedAdmin = null): array
|
||||
{
|
||||
$rows = [
|
||||
['转账单号', '玩家ID', '用户名', '昵称', '方向', '币种', '金额', '状态', '外部单号', '失败原因', '创建时间', '完成时间'],
|
||||
@@ -852,6 +962,10 @@ final class AdminReportQueryService
|
||||
->with(['player:id,username,nickname'])
|
||||
->orderByDesc('id');
|
||||
|
||||
if ($scopedAdmin !== null) {
|
||||
AdminDataScope::applyEloquentViaPlayer($query, $scopedAdmin);
|
||||
}
|
||||
|
||||
$playerId = isset($filterJson['player_id']) ? (int) $filterJson['player_id'] : null;
|
||||
if ($playerId !== null && $playerId > 0) {
|
||||
$query->where('player_id', $playerId);
|
||||
@@ -884,7 +998,7 @@ final class AdminReportQueryService
|
||||
/**
|
||||
* @return list<array<int, string|int|float|null>>
|
||||
*/
|
||||
private function walletTxnsExportRows(?array $filterJson, string $dateFrom, string $dateTo): array
|
||||
private function walletTxnsExportRows(?array $filterJson, string $dateFrom, string $dateTo, ?AdminUser $scopedAdmin = null): array
|
||||
{
|
||||
$rows = [
|
||||
['流水号', '玩家ID', '用户名', '业务类型', '业务单号', '方向', '金额', '变动前余额', '变动后余额', '状态', '外部单号', '备注', '创建时间'],
|
||||
@@ -894,6 +1008,10 @@ final class AdminReportQueryService
|
||||
->with(['player:id,username'])
|
||||
->orderByDesc('id');
|
||||
|
||||
if ($scopedAdmin !== null) {
|
||||
AdminDataScope::applyEloquentViaPlayer($query, $scopedAdmin);
|
||||
}
|
||||
|
||||
$playerId = isset($filterJson['player_id']) ? (int) $filterJson['player_id'] : null;
|
||||
if ($playerId !== null && $playerId > 0) {
|
||||
$query->where('player_id', $playerId);
|
||||
|
||||
85
app/Services/Agent/AgentAdminUserService.php
Normal file
85
app/Services/Agent/AgentAdminUserService.php
Normal file
@@ -0,0 +1,85 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Agent;
|
||||
|
||||
use App\Models\AdminRole;
|
||||
use App\Models\AdminUser;
|
||||
use App\Models\AgentNode;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
|
||||
final class AgentAdminUserService
|
||||
{
|
||||
/**
|
||||
* @param array{
|
||||
* username: string,
|
||||
* nickname: string,
|
||||
* email?: ?string,
|
||||
* password: string,
|
||||
* status?: int,
|
||||
* role_ids?: list<int>
|
||||
* } $payload
|
||||
*/
|
||||
public function createUnderAgent(AgentNode $agent, array $payload): AdminUser
|
||||
{
|
||||
$roleIds = array_values(array_unique(array_map('intval', $payload['role_ids'] ?? [])));
|
||||
$this->assertRolesAssignable($agent, $roleIds);
|
||||
|
||||
return DB::transaction(function () use ($agent, $payload, $roleIds): AdminUser {
|
||||
$user = AdminUser::query()->create([
|
||||
'username' => $payload['username'],
|
||||
'name' => $payload['nickname'],
|
||||
'email' => isset($payload['email']) ? trim((string) $payload['email']) : null,
|
||||
'password' => $payload['password'],
|
||||
'status' => (int) ($payload['status'] ?? 0),
|
||||
]);
|
||||
|
||||
DB::table('admin_user_agents')->insert([
|
||||
'admin_user_id' => $user->id,
|
||||
'agent_node_id' => $agent->id,
|
||||
'is_primary' => true,
|
||||
'granted_at' => now(),
|
||||
]);
|
||||
|
||||
$user->syncAgentRoleIds($agent->id, $roleIds);
|
||||
|
||||
return $user->fresh();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<int> $roleIds
|
||||
*/
|
||||
public function syncRoles(AgentNode $agent, AdminUser $user, array $roleIds): AdminUser
|
||||
{
|
||||
if ((int) $user->primaryAgentNodeId() !== (int) $agent->id) {
|
||||
throw ValidationException::withMessages(['user' => ['agent_mismatch']]);
|
||||
}
|
||||
|
||||
$roleIds = array_values(array_unique(array_map('intval', $roleIds)));
|
||||
$this->assertRolesAssignable($agent, $roleIds);
|
||||
$user->syncAgentRoleIds($agent->id, $roleIds);
|
||||
|
||||
return $user->fresh();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<int> $roleIds
|
||||
*/
|
||||
private function assertRolesAssignable(AgentNode $agent, array $roleIds): void
|
||||
{
|
||||
if ($roleIds === []) {
|
||||
return;
|
||||
}
|
||||
|
||||
$validCount = AdminRole::query()
|
||||
->where('scope_type', AdminRole::SCOPE_AGENT)
|
||||
->where('owner_agent_id', $agent->id)
|
||||
->whereIn('id', $roleIds)
|
||||
->count();
|
||||
|
||||
if ($validCount !== count($roleIds)) {
|
||||
throw ValidationException::withMessages(['role_ids' => ['invalid_for_agent']]);
|
||||
}
|
||||
}
|
||||
}
|
||||
98
app/Services/Agent/AgentDelegationService.php
Normal file
98
app/Services/Agent/AgentDelegationService.php
Normal file
@@ -0,0 +1,98 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Agent;
|
||||
|
||||
use App\Models\AdminUser;
|
||||
use App\Models\AgentNode;
|
||||
use App\Support\AgentDelegationAuthorization;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
final class AgentDelegationService
|
||||
{
|
||||
/**
|
||||
* @return list<array{
|
||||
* menu_action_id: int,
|
||||
* permission_code: string,
|
||||
* name: string,
|
||||
* can_delegate: bool
|
||||
* }>
|
||||
*/
|
||||
public function listForChild(AgentNode $child, ?AdminUser $actor = null): array
|
||||
{
|
||||
$rows = DB::table('agent_delegation_grants as g')
|
||||
->join('admin_menu_actions as ma', 'ma.id', '=', 'g.menu_action_id')
|
||||
->where('g.child_agent_id', $child->id)
|
||||
->where('ma.status', 1)
|
||||
->orderBy('ma.permission_code')
|
||||
->get(['g.menu_action_id', 'g.can_delegate', 'ma.permission_code', 'ma.name']);
|
||||
|
||||
if ($rows->isNotEmpty()) {
|
||||
return $rows->map(static fn ($row): array => [
|
||||
'menu_action_id' => (int) $row->menu_action_id,
|
||||
'permission_code' => (string) $row->permission_code,
|
||||
'name' => (string) $row->name,
|
||||
'can_delegate' => (bool) $row->can_delegate,
|
||||
])->all();
|
||||
}
|
||||
|
||||
if ($actor === null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$codes = $actor->effectiveMenuActionPermissionCodes();
|
||||
if ($codes === []) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return DB::table('admin_menu_actions')
|
||||
->where('status', 1)
|
||||
->whereIn('permission_code', $codes)
|
||||
->orderBy('permission_code')
|
||||
->get(['id', 'permission_code', 'name'])
|
||||
->map(static fn ($row): array => [
|
||||
'menu_action_id' => (int) $row->id,
|
||||
'permission_code' => (string) $row->permission_code,
|
||||
'name' => (string) $row->name,
|
||||
'can_delegate' => false,
|
||||
])
|
||||
->all();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<array{menu_action_id: int, can_delegate?: bool}> $grants
|
||||
* @return list<array{menu_action_id: int, permission_code: string, name: string, can_delegate: bool}>
|
||||
*/
|
||||
public function syncGrants(AdminUser $actor, AgentNode $child, array $grants): array
|
||||
{
|
||||
AgentDelegationAuthorization::assertGrantsAllowed($actor, $child, $grants);
|
||||
|
||||
$parentId = (int) ($child->parent_id ?? 0);
|
||||
if ($parentId <= 0) {
|
||||
throw new \InvalidArgumentException('Child agent must have parent.');
|
||||
}
|
||||
|
||||
$now = Carbon::now();
|
||||
|
||||
DB::transaction(function () use ($actor, $child, $grants, $parentId, $now): void {
|
||||
DB::table('agent_delegation_grants')
|
||||
->where('child_agent_id', $child->id)
|
||||
->delete();
|
||||
|
||||
foreach ($grants as $grant) {
|
||||
DB::table('agent_delegation_grants')->insert([
|
||||
'parent_agent_id' => $parentId,
|
||||
'child_agent_id' => $child->id,
|
||||
'menu_action_id' => (int) $grant['menu_action_id'],
|
||||
'can_delegate' => ! empty($grant['can_delegate']),
|
||||
'granted_by' => $actor->id,
|
||||
'granted_at' => $now,
|
||||
'created_at' => $now,
|
||||
'updated_at' => $now,
|
||||
]);
|
||||
}
|
||||
});
|
||||
|
||||
return $this->listForChild($child->fresh());
|
||||
}
|
||||
}
|
||||
82
app/Services/Agent/AgentNodeService.php
Normal file
82
app/Services/Agent/AgentNodeService.php
Normal file
@@ -0,0 +1,82 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Agent;
|
||||
|
||||
use App\Models\AdminUser;
|
||||
use App\Models\AgentNode;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
|
||||
final class AgentNodeService
|
||||
{
|
||||
/**
|
||||
* @param array{parent_id: int, code: string, name: string, status?: int} $payload
|
||||
*/
|
||||
public function createChild(AdminUser $actor, array $payload): AgentNode
|
||||
{
|
||||
$parent = AgentNode::query()->findOrFail((int) $payload['parent_id']);
|
||||
$code = trim((string) $payload['code']);
|
||||
$name = trim((string) $payload['name']);
|
||||
$status = (int) ($payload['status'] ?? 1);
|
||||
|
||||
if ($code === '' || $name === '') {
|
||||
throw ValidationException::withMessages([
|
||||
'code' => ['required'],
|
||||
'name' => ['required'],
|
||||
]);
|
||||
}
|
||||
|
||||
if (AgentNode::query()->where('admin_site_id', $parent->admin_site_id)->where('code', $code)->exists()) {
|
||||
throw ValidationException::withMessages([
|
||||
'code' => ['unique'],
|
||||
]);
|
||||
}
|
||||
|
||||
return DB::transaction(function () use ($actor, $parent, $code, $name, $status): AgentNode {
|
||||
$node = AgentNode::query()->create([
|
||||
'admin_site_id' => $parent->admin_site_id,
|
||||
'parent_id' => $parent->id,
|
||||
'path' => '/',
|
||||
'depth' => (int) $parent->depth + 1,
|
||||
'code' => $code,
|
||||
'name' => $name,
|
||||
'status' => $status === 0 ? 0 : 1,
|
||||
'created_by' => $actor->id,
|
||||
'extra_json' => null,
|
||||
]);
|
||||
|
||||
$node->path = (string) $parent->path.$node->id.'/';
|
||||
$node->save();
|
||||
|
||||
return $node->fresh(['adminSite']);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array{name?: string, status?: int} $payload
|
||||
*/
|
||||
public function update(AgentNode $node, array $payload): AgentNode
|
||||
{
|
||||
if (array_key_exists('name', $payload)) {
|
||||
$name = trim((string) $payload['name']);
|
||||
if ($name !== '') {
|
||||
$node->name = $name;
|
||||
}
|
||||
}
|
||||
|
||||
if (array_key_exists('status', $payload)) {
|
||||
$node->status = (int) $payload['status'] === 0 ? 0 : 1;
|
||||
}
|
||||
|
||||
$node->save();
|
||||
|
||||
return $node->fresh(['adminSite']);
|
||||
}
|
||||
|
||||
public function destroy(AgentNode $node): void
|
||||
{
|
||||
DB::transaction(static function () use ($node): void {
|
||||
$node->delete();
|
||||
});
|
||||
}
|
||||
}
|
||||
102
app/Services/Agent/AgentRoleService.php
Normal file
102
app/Services/Agent/AgentRoleService.php
Normal file
@@ -0,0 +1,102 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Agent;
|
||||
|
||||
use App\Models\AdminRole;
|
||||
use App\Models\AdminUser;
|
||||
use App\Models\AgentNode;
|
||||
use App\Support\AgentRoleAuthorization;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
|
||||
final class AgentRoleService
|
||||
{
|
||||
/**
|
||||
* @param array{slug: string, name: string, description?: ?string, status?: int, permission_slugs?: list<string>} $payload
|
||||
*/
|
||||
public function createForAgent(AdminUser $actor, AgentNode $owner, array $payload): AdminRole
|
||||
{
|
||||
AgentRoleAuthorization::assertSlugsForAgentRole(
|
||||
$actor,
|
||||
$owner,
|
||||
array_values(array_unique($payload['permission_slugs'] ?? [])),
|
||||
);
|
||||
|
||||
$slug = trim((string) $payload['slug']);
|
||||
if (AdminRole::query()
|
||||
->where('owner_agent_id', $owner->id)
|
||||
->where('slug', $slug)
|
||||
->exists()) {
|
||||
throw ValidationException::withMessages(['slug' => ['unique']]);
|
||||
}
|
||||
|
||||
return DB::transaction(function () use ($payload, $owner, $slug): AdminRole {
|
||||
$role = AdminRole::query()->create([
|
||||
'slug' => $slug,
|
||||
'code' => $slug,
|
||||
'name' => trim((string) $payload['name']),
|
||||
'description' => $payload['description'] ?? null,
|
||||
'status' => (int) ($payload['status'] ?? 1) === 0 ? 0 : 1,
|
||||
'is_system' => false,
|
||||
'sort_order' => 0,
|
||||
'scope_type' => AdminRole::SCOPE_AGENT,
|
||||
'owner_agent_id' => $owner->id,
|
||||
'delegated_from_role_id' => null,
|
||||
]);
|
||||
|
||||
$role->syncLegacyPermissionSlugs($payload['permission_slugs'] ?? []);
|
||||
|
||||
return $role->fresh();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array{name?: string, description?: ?string, status?: int} $payload
|
||||
*/
|
||||
public function update(AdminRole $role, array $payload): AdminRole
|
||||
{
|
||||
if (array_key_exists('name', $payload)) {
|
||||
$name = trim((string) $payload['name']);
|
||||
if ($name !== '') {
|
||||
$role->name = $name;
|
||||
}
|
||||
}
|
||||
|
||||
if (array_key_exists('description', $payload)) {
|
||||
$role->description = $payload['description'];
|
||||
}
|
||||
|
||||
if (array_key_exists('status', $payload)) {
|
||||
$role->status = (int) $payload['status'] === 0 ? 0 : 1;
|
||||
}
|
||||
|
||||
$role->save();
|
||||
|
||||
return $role->fresh();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<string> $permissionSlugs
|
||||
*/
|
||||
public function syncPermissions(AdminUser $actor, AdminRole $role, array $permissionSlugs): AdminRole
|
||||
{
|
||||
$owner = AgentNode::query()->findOrFail((int) $role->owner_agent_id);
|
||||
AgentRoleAuthorization::assertSlugsForAgentRole($actor, $owner, $permissionSlugs);
|
||||
$role->syncLegacyPermissionSlugs($permissionSlugs);
|
||||
|
||||
return $role->fresh();
|
||||
}
|
||||
|
||||
public function destroy(AdminRole $role): void
|
||||
{
|
||||
if ($role->is_system) {
|
||||
throw ValidationException::withMessages(['role' => ['system_role']]);
|
||||
}
|
||||
|
||||
if ($role->assignedUserCount() > 0) {
|
||||
throw ValidationException::withMessages(['role' => ['in_use']]);
|
||||
}
|
||||
|
||||
$role->delete();
|
||||
}
|
||||
}
|
||||
@@ -160,6 +160,43 @@ final class LotterySettings
|
||||
Cache::put(self::cacheKey($key), self::normalizeValue($value), self::cacheTtlSeconds());
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量写入(管理端一次保存多块配置,避免 N 次 HTTP + 审计)。
|
||||
*
|
||||
* @param list<array{key: string, value: mixed}> $items
|
||||
*/
|
||||
public static function putMany(array $items): void
|
||||
{
|
||||
if ($items === []) {
|
||||
return;
|
||||
}
|
||||
|
||||
$keys = array_values(array_unique(array_map(
|
||||
static fn (array $item): string => (string) $item['key'],
|
||||
$items,
|
||||
)));
|
||||
|
||||
/** @var array<string, LotterySetting> $existing */
|
||||
$existing = LotterySetting::query()
|
||||
->whereIn('setting_key', $keys)
|
||||
->get()
|
||||
->keyBy('setting_key')
|
||||
->all();
|
||||
|
||||
foreach ($items as $item) {
|
||||
$key = (string) $item['key'];
|
||||
$value = $item['value'];
|
||||
$row = $existing[$key] ?? null;
|
||||
|
||||
self::put(
|
||||
$key,
|
||||
$value,
|
||||
$row ? (string) $row->group_name : (explode('.', $key)[0] ?? 'general'),
|
||||
$row ? $row->description_zh : null,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public static function cacheKey(string $key): string
|
||||
{
|
||||
return 'lottery_settings:'.$key;
|
||||
|
||||
Reference in New Issue
Block a user