Files
lotteryLaravel/app/Support/AuditLogApiPresenter.php
wchino 15bd997c4e
Some checks failed
lotterLaravel CI / test (push) Has been cancelled
lotterLaravel E2E / e2e-api (push) Has been cancelled
feat(core): harden sessions settlement and credit activity
2026-07-22 20:53:43 +08:00

412 lines
14 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
namespace App\Support;
use App\Models\Player;
use App\Models\AuditLog;
use App\Models\AdminUser;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
/**
* 审计日志列表展示:模块 / 动作 / 目标中文标签API 资源名 + 业务码映射)。
*/
final class AuditLogApiPresenter
{
/** @var array<string, string> */
private const MODULE_LABELS = [
'agent' => '代理管理',
'system' => '系统管理',
'settings' => '系统设置',
'integration' => '接入站点',
'player_manage' => '玩家管理',
'player_service' => '玩家服务',
'risk_cap' => '限额规则',
'odds' => '赔率配置',
'play_config' => '玩法配置',
'settlement' => '派彩结算',
'report_jobs' => '报表导出',
'reconcile_jobs' => '对账任务',
'draw' => '开奖期号',
'wallet' => '钱包资金',
'reconcile' => '对账核对',
'jackpot' => '奖池',
'dashboard' => '工作台',
'job' => '后台任务',
'bootstrap' => '系统',
];
/** @var array<string, string> */
private const ACTION_LABELS = [
'agent_role.sync_permissions' => '同步代理角色权限',
'admin_role.sync_permissions' => '同步平台角色权限',
'admin_role.create' => '创建平台角色',
'admin_role.update' => '更新平台角色',
'admin_role.delete' => '删除平台角色',
'agent_role.create' => '创建代理角色',
'agent_role.update' => '更新代理角色',
'agent_role.destroy' => '删除代理角色',
'agent_admin_user.create' => '创建代理账号',
'agent_admin_user.sync_roles' => '同步代理账号角色',
'agent_node.create' => '创建代理节点',
'agent_node.update' => '更新代理节点',
'agent_node.destroy' => '删除代理节点',
'agent.delegation.sync' => '同步代理授权',
'admin_user.create' => '创建管理员',
'admin_user.update' => '更新管理员',
'admin_user.delete' => '删除管理员',
'sync_permissions' => '同步权限',
'batch_update' => '批量更新设置',
'payout_adjustment' => '派彩调整',
'auto_payout' => '自动派彩成功',
'auto_payout_failed' => '自动派彩失败',
'rotate_secrets' => '轮换密钥',
'toggle_active' => '切换启用状态',
'enqueue' => '提交报表导出',
'freeze' => '冻结玩家',
'unfreeze' => '解冻玩家',
'publish' => '发布开奖结果',
'reopen' => '重新开奖',
'run' => '执行结算',
'confirm' => '确认账单',
'close' => '关账',
'payment' => '登记回款',
'write_off' => '坏账核销',
'adjustment' => '调整账单',
];
/** @var array<string, string> */
private const TARGET_TYPE_LABELS = [
'admin_role' => '角色',
'admin_user' => '管理员',
'agent_node' => '代理节点',
'admin_site' => '对接站点',
'player' => '玩家',
'lottery_settings' => '系统设置',
'lottery_setting' => '设置项',
'risk_cap_version' => '风险池版本',
'odds_version' => '赔率版本',
'play_config_item' => '玩法',
'play_config_version' => '玩法配置版本',
'settlement_batch_adjustment' => '结算调整单',
'settlement_batch' => '结算批次',
'report_job' => '报表任务',
'reconcile_job' => '对账任务',
'transfer_no' => '转账单',
'draw' => '期次',
'batch' => '批次',
'agent_settlement_bill' => '代理结算单',
'agent_settlement_period' => '代理结算期',
'integration_site' => '接入站点',
'jackpot_pool' => '奖池',
];
/** @var array<string, string> */
private const API_RESOURCE_ENTITY_LABELS = [
'admin-users' => '管理员',
'admin-roles' => '角色',
'agent-nodes' => '代理',
'agent-roles' => '代理角色',
'agent-admin-users' => '代理账号',
'agent-lines' => '代理线路',
'agent-delegation-grants' => '代理授权',
'draws' => '期次',
'players' => '玩家',
'settings' => '系统设置',
'integration-sites' => '接入站点',
'reconcile-jobs' => '对账任务',
'report-jobs' => '报表任务',
'settlement-batches' => '结算批次',
'jackpot' => '奖池',
'risk' => '风控',
];
/** @var array<string, string> */
private const VERB_LABELS = [
'sync' => '同步',
'store' => '创建',
'update' => '更新',
'destroy' => '删除',
'delete' => '删除',
'create' => '创建',
'publish' => '发布',
'reopen' => '重新开奖',
'freeze' => '冻结',
'unfreeze' => '解冻',
'run' => '执行',
'transfer' => '转账',
'start' => '开始',
'test' => '测试',
];
/**
* @param LengthAwarePaginator<AuditLog> $paginator
* @return array{items: list<array<string, mixed>>, meta: array{current_page: int, per_page: int, total: int, last_page: int}}
*/
public static function listPayload(LengthAwarePaginator $paginator): array
{
$items = collect($paginator->items());
$resourceNames = self::loadResourceNames($items);
$operatorLabels = self::loadOperatorLabels($items);
return AdminApiList::payload(
$paginator,
fn (AuditLog $r) => self::row($r, $resourceNames, $operatorLabels),
);
}
/**
* @param array<string, string> $resourceNames
* @param array<string, string> $operatorLabels
* @return array<string, mixed>
*/
public static function row(AuditLog $r, array $resourceNames = [], array $operatorLabels = []): array
{
$operatorDisplay = self::operatorDisplay($r, $operatorLabels);
return [
'id' => (int) $r->id,
'operator_type' => $r->operator_type,
'operator_id' => (int) $r->operator_id,
'operator_label' => $operatorDisplay['label'],
'operator_subtitle' => $operatorDisplay['subtitle'],
'module_code' => $r->module_code,
'action_code' => $r->action_code,
'target_type' => $r->target_type,
'target_id' => $r->target_id,
'module_label' => self::moduleLabel($r->module_code),
'action_label' => self::actionLabel($r, $resourceNames),
'target_label' => self::targetLabel($r, $resourceNames),
'summary_label' => self::summaryLabel($r, $resourceNames),
'before_json' => $r->before_json,
'after_json' => $r->after_json,
'ip' => $r->ip,
'user_agent' => $r->user_agent,
'created_at' => $r->created_at?->toIso8601String(),
];
}
/**
* @param Collection<int, AuditLog> $items
* @return array<string, string>
*/
private static function loadOperatorLabels(Collection $items): array
{
$labels = [];
$adminIds = $items
->filter(fn (AuditLog $row) => $row->operator_type === 'admin' && (int) $row->operator_id > 0)
->pluck('operator_id')
->map(fn (mixed $id): int => (int) $id)
->unique()
->values()
->all();
if ($adminIds !== []) {
$admins = AdminUser::query()
->whereIn('id', $adminIds)
->get(['id', 'username', 'name']);
foreach ($admins as $admin) {
$labels['admin:'.$admin->id] = self::formatIdentityLabel(
(string) ($admin->name ?? ''),
(string) ($admin->username ?? ''),
);
}
}
$playerIds = $items
->filter(fn (AuditLog $row) => $row->operator_type === 'player' && (int) $row->operator_id > 0)
->pluck('operator_id')
->map(fn (mixed $id): int => (int) $id)
->unique()
->values()
->all();
if ($playerIds !== []) {
$players = Player::query()
->whereIn('id', $playerIds)
->get(['id', 'username', 'nickname']);
foreach ($players as $player) {
$labels['player:'.$player->id] = self::formatIdentityLabel(
(string) ($player->nickname ?? ''),
(string) ($player->username ?? ''),
);
}
}
return $labels;
}
/**
* @param Collection<int, AuditLog> $items
* @return array<string, string>
*/
private static function loadResourceNames(Collection $items): array
{
$codes = $items
->pluck('target_type')
->filter(fn (?string $type) => self::isApiResourceCode($type))
->unique()
->values()
->all();
if ($codes === []) {
return [];
}
/** @var array<string, string> */
return DB::table('admin_api_resources')
->whereIn('code', $codes)
->pluck('name', 'code')
->all();
}
private static function moduleLabel(?string $code): string
{
if ($code === null || $code === '') {
return '—';
}
return self::MODULE_LABELS[$code] ?? $code;
}
/**
* @param array<string, string> $operatorLabels
*/
private static function operatorDisplay(AuditLog $r, array $operatorLabels): array
{
$type = (string) ($r->operator_type ?? '');
$id = (int) ($r->operator_id ?? 0);
if ($type === 'system') {
return ['label' => '系统', 'subtitle' => null];
}
$key = $type.':'.$id;
if (isset($operatorLabels[$key]) && $operatorLabels[$key] !== '') {
return [
'label' => $operatorLabels[$key],
'subtitle' => $id > 0 ? sprintf('%s #%d', $type === 'admin' ? '管理员' : ($type === 'player' ? '玩家' : $type), $id) : null,
];
}
$fallbackLabel = match ($type) {
'admin' => $id > 0 ? '管理员 #'.$id : '管理员',
'player' => $id > 0 ? '玩家 #'.$id : '玩家',
'system' => '系统',
default => $id > 0 ? sprintf('%s #%d', $type !== '' ? $type : '未知', $id) : ($type !== '' ? $type : '未知'),
};
return ['label' => $fallbackLabel, 'subtitle' => null];
}
private static function formatIdentityLabel(string $primary, string $secondary): string
{
$primary = trim($primary);
$secondary = trim($secondary);
if ($primary !== '') {
return $primary;
}
return $secondary;
}
/**
* @param array<string, string> $resourceNames
*/
private static function actionLabel(AuditLog $r, array $resourceNames): string
{
$action = (string) ($r->action_code ?? '');
if ($action === '') {
return '—';
}
if (isset(self::ACTION_LABELS[$action])) {
return self::ACTION_LABELS[$action];
}
$targetType = (string) ($r->target_type ?? '');
if (self::isApiResourceCode($targetType) && isset($resourceNames[$targetType])) {
return $resourceNames[$targetType];
}
if (isset(self::VERB_LABELS[$action])) {
return self::VERB_LABELS[$action];
}
return $action;
}
/**
* @param array<string, string> $resourceNames
*/
private static function targetLabel(AuditLog $r, array $resourceNames): string
{
$type = (string) ($r->target_type ?? '');
$id = trim((string) ($r->target_id ?? ''));
if ($type === '') {
return $id !== '' ? '#'.$id : '—';
}
if (self::isApiResourceCode($type)) {
$entity = self::entityLabelFromResourceCode($type, $resourceNames);
return $id !== '' ? sprintf('%s #%s', $entity, $id) : $entity;
}
$typeLabel = self::TARGET_TYPE_LABELS[$type] ?? $type;
return $id !== '' ? sprintf('%s #%s', $typeLabel, $id) : $typeLabel;
}
/**
* @param array<string, string> $resourceNames
*/
private static function summaryLabel(AuditLog $r, array $resourceNames): string
{
$module = self::moduleLabel($r->module_code);
$action = self::actionLabel($r, $resourceNames);
$target = self::targetLabel($r, $resourceNames);
if ($action === '—') {
return $module;
}
if ($target === '—') {
return sprintf('%s · %s', $module, $action);
}
if ($target === $action || str_starts_with($target, $action.' #')) {
return sprintf('%s · %s', $module, $action);
}
return sprintf('%s · %s%s', $module, $action, $target);
}
/**
* @param array<string, string> $resourceNames
*/
private static function entityLabelFromResourceCode(string $code, array $resourceNames): string
{
$segments = explode('.', $code);
if (count($segments) < 2) {
return $resourceNames[$code] ?? $code;
}
$resourceSegment = $segments[1] ?? '';
if (isset(self::API_RESOURCE_ENTITY_LABELS[$resourceSegment])) {
return self::API_RESOURCE_ENTITY_LABELS[$resourceSegment];
}
return $resourceNames[$code] ?? $resourceSegment;
}
private static function isApiResourceCode(?string $code): bool
{
return is_string($code) && str_starts_with($code, 'admin.');
}
}