feat(admin): 密码校验统一与后台 API/设置增强
Some checks failed
lotterLaravel CI / test (push) Has been cancelled
lotterLaravel E2E / e2e-api (push) Has been cancelled

- 管理端账号/代理/站点等密码最短长度统一为 6 位
- LotterySettings、审计日志展示、币种与报表接口等小修复
- 补充设置批量更新、注单、校验错误等相关测试
This commit is contained in:
2026-06-26 14:39:22 +08:00
parent 8d5ff0a7ae
commit 3f04b4ebe3
28 changed files with 283 additions and 69 deletions

View File

@@ -24,7 +24,7 @@ final class AdminAuthorizationRegistry
return [
['slug' => 'prd.dashboard.view', 'name' => '仪表盘·查看', 'nav_segment' => 'dashboard', 'permission_codes' => ['dashboard.view']],
['slug' => 'prd.admin_user.manage', 'name' => '管理员列表·可管理', 'nav_segment' => 'admin_users', 'permission_codes' => ['system.admin_user.manage']],
['slug' => 'prd.admin_user.manage', 'name' => '平台账号·可管理', 'nav_segment' => 'admin_users', 'permission_codes' => ['system.admin_user.manage']],
['slug' => 'prd.admin_role.manage', 'name' => '角色管理·可管理', 'nav_segment' => 'admin_roles', 'permission_codes' => ['system.admin_role.manage']],
['slug' => 'prd.agent.view', 'name' => '代理管理·查看', 'nav_segment' => 'agents', 'permission_codes' => ['agent.node.view']],
@@ -92,7 +92,7 @@ final class AdminAuthorizationRegistry
{
$labels = [
'dashboard' => '仪表盘',
'admin_users' => '管理列表',
'admin_users' => '平台账号',
'admin_roles' => '角色管理',
'players' => '玩家列表',
'currencies' => '币种管理',
@@ -110,7 +110,7 @@ final class AdminAuthorizationRegistry
'audit' => '审计日志',
'settings' => '系统设置',
'integration' => '接入站点',
'agents' => '代理线路',
'agents' => '代理管理',
];
return array_map(
@@ -147,8 +147,7 @@ final class AdminAuthorizationRegistry
{
return [
['segment' => 'dashboard', 'label' => 'Dashboard', 'href' => '/admin', 'nav_group' => 'overview', 'requiredAny' => ['prd.dashboard.view']],
['segment' => 'agents', 'label' => 'Agent lines', 'href' => '/admin/agents', 'nav_group' => 'agent', 'activeExact' => true, 'requiredAny' => array_values(array_unique(array_merge(['prd.agent.view', 'prd.agent.manage', 'prd.agent.role.view', 'prd.agent.role.manage', 'prd.agent.user.view', 'prd.agent.user.manage', 'prd.agent-line.provision', 'prd.agent.profile.manage'], AdminPermissionLanguage::requiredAnyPrdSlugs('integration-sites'))))],
['segment' => 'agent_list', 'label' => 'Agent list', 'href' => '/admin/agents/list', 'nav_group' => 'agent', 'activeMatchPrefix' => '/admin/agents/list', 'requiredAny' => ['prd.agent.view', 'prd.agent.manage', 'prd.agent.role.view', 'prd.agent.role.manage', 'prd.agent.user.view', 'prd.agent.user.manage', 'prd.agent.profile.manage']],
['segment' => 'agents', 'label' => 'Agent management', 'href' => '/admin/agents', 'nav_group' => 'agent', 'activeMatchPrefix' => '/admin/agents', 'requiredAny' => array_values(array_unique(array_merge(['prd.agent.view', 'prd.agent.manage', 'prd.agent.role.view', 'prd.agent.role.manage', 'prd.agent.user.view', 'prd.agent.user.manage', 'prd.agent-line.provision', 'prd.agent.profile.manage'], AdminPermissionLanguage::requiredAnyPrdSlugs('integration-sites'))))],
['segment' => 'settlement_center', 'label' => 'Credit settlement', 'href' => '/admin/settlement-center', 'nav_group' => 'agent', 'activeMatchPrefix' => '/admin/settlement-center', 'requiredAny' => ['prd.settlement.agent.view', 'prd.settlement.agent.manage']],
['segment' => 'draws', 'label' => 'Draws', 'href' => '/admin/draws', 'nav_group' => 'operations', 'requiredAny' => ['prd.draw_result.manage', 'prd.draw_result.view', 'prd.draw_reopen.manage']],
['segment' => 'tickets', 'label' => 'Tickets', 'href' => '/admin/tickets', 'nav_group' => 'operations', 'requiredAny' => ['prd.tickets.view']],
@@ -483,6 +482,7 @@ final class AdminAuthorizationRegistry
['code' => 'admin.config.risk-cap-versions.publish', 'module_code' => 'config', 'name' => '发布封顶版本', 'http_method' => 'POST', 'uri_pattern' => '/api/v1/admin/config/risk-cap-versions/{id}/publish', 'route_name' => 'api.v1.admin.config.risk-cap-versions.publish', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'legacy_permission_slugs' => ['prd.risk_cap.manage']],
['code' => 'admin.config.risk-cap-versions.destroy', 'module_code' => 'config', 'name' => '删除封顶版本', 'http_method' => 'DELETE', 'uri_pattern' => '/api/v1/admin/config/risk-cap-versions/{id}', 'route_name' => 'api.v1.admin.config.risk-cap-versions.destroy', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'legacy_permission_slugs' => ['prd.risk_cap.manage']],
['code' => 'admin.settings.index', 'module_code' => 'settings', 'name' => '系统设置列表', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/settings', 'route_name' => 'api.v1.admin.settings.index', 'auth_mode' => 'permission_required', 'is_audit_required' => false, 'legacy_permission_slugs' => ['prd.wallet_reconcile.manage', 'prd.rebate.manage', 'prd.rebate.view', 'prd.payout.manage']],
['code' => 'admin.settings.deployment', 'module_code' => 'settings', 'name' => '部署参数只读', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/settings/deployment', 'route_name' => 'api.v1.admin.settings.deployment', 'auth_mode' => 'permission_required', 'is_audit_required' => false, 'legacy_permission_slugs' => ['prd.wallet_reconcile.manage', 'prd.rebate.manage', 'prd.rebate.view', 'prd.payout.manage', 'prd.draw_result.manage', 'prd.draw_result.view']],
['code' => 'admin.settings.batch-update', 'module_code' => 'settings', 'name' => '系统设置批量更新', 'http_method' => 'PUT', 'uri_pattern' => '/api/v1/admin/settings/batch', 'route_name' => 'api.v1.admin.settings.batch-update', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'permission_codes' => ['service.wallet.manage', 'service.reconcile.manage', 'service.wallet.adjust', 'config.odds.manage', 'settlement.batch.manage', 'draw.review.review', 'draw.review.publish']],
['code' => 'admin.settings.update', 'module_code' => 'settings', 'name' => '系统设置更新', 'http_method' => 'PUT', 'uri_pattern' => '/api/v1/admin/settings/{key}', 'route_name' => 'api.v1.admin.settings.update', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'permission_codes' => ['service.wallet.manage', 'service.reconcile.manage', 'service.wallet.adjust', 'config.odds.manage', 'settlement.batch.manage', 'draw.review.review', 'draw.review.publish']],
['code' => 'admin.currencies.index', 'module_code' => 'settings', 'name' => '币种列表', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/currencies', 'route_name' => 'api.v1.admin.currencies.index', 'auth_mode' => 'permission_required', 'is_audit_required' => false, 'legacy_permission_slugs' => ['prd.currency.manage']],

View File

@@ -17,6 +17,10 @@ final class AdminSettingPolicy
public static function canUpdate(AdminUser $admin, string $key): bool
{
if (LotterySettings::isEnvLocked($key)) {
return false;
}
if (str_starts_with($key, 'settlement.')) {
return $admin->hasAdminPermission('prd.payout.manage');
}
@@ -46,6 +50,12 @@ final class AdminSettingPolicy
$values = [];
foreach ($items as $index => $item) {
$key = (string) $item['key'];
if (LotterySettings::isEnvLocked($key)) {
throw ValidationException::withMessages([
"items.$index.key" => ['该参数由服务器环境变量配置,不能在后台修改。'],
]);
}
if (! in_array($key, self::WALLET_LIMIT_KEYS, true)) {
continue;
}

View File

@@ -16,21 +16,24 @@ final class AuditLogApiPresenter
{
/** @var array<string, string> */
private const MODULE_LABELS = [
'agent' => '代理',
'system' => '系统',
'agent' => '代理管理',
'system' => '系统管理',
'settings' => '系统设置',
'integration' => '接站点',
'integration' => '接站点',
'player_manage' => '玩家管理',
'risk_cap' => '风险池',
'odds' => '赔率',
'player_service' => '玩家服务',
'risk_cap' => '限额规则',
'odds' => '赔率配置',
'play_config' => '玩法配置',
'settlement' => '结算',
'report_jobs' => '报表任务',
'settlement' => '派彩结算',
'report_jobs' => '报表导出',
'reconcile_jobs' => '对账任务',
'draw' => '开奖',
'wallet' => '钱包',
'reconcile' => '对账',
'job' => '任务',
'draw' => '开奖期号',
'wallet' => '钱包资金',
'reconcile' => '对账核对',
'jackpot' => '奖池',
'dashboard' => '工作台',
'job' => '后台任务',
'bootstrap' => '系统',
];
@@ -58,7 +61,17 @@ final class AuditLogApiPresenter
'payout_adjustment' => '派彩调整',
'rotate_secrets' => '轮换密钥',
'toggle_active' => '切换启用状态',
'enqueue' => '提交报表任务',
'enqueue' => '提交报表导出',
'freeze' => '冻结玩家',
'unfreeze' => '解冻玩家',
'publish' => '发布开奖结果',
'reopen' => '重新开奖',
'run' => '执行结算',
'confirm' => '确认账单',
'close' => '关账',
'payment' => '登记回款',
'write_off' => '坏账核销',
'adjustment' => '调整账单',
];
/** @var array<string, string> */
@@ -80,6 +93,30 @@ final class AuditLogApiPresenter
'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> */
@@ -138,6 +175,7 @@ final class AuditLogApiPresenter
'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,
@@ -311,9 +349,9 @@ final class AuditLogApiPresenter
}
if (self::isApiResourceCode($type)) {
$name = $resourceNames[$type] ?? $type;
$entity = self::entityLabelFromResourceCode($type, $resourceNames);
return $id !== '' ? sprintf('%s #%s', $name, $id) : $name;
return $id !== '' ? sprintf('%s #%s', $entity, $id) : $entity;
}
$typeLabel = self::TARGET_TYPE_LABELS[$type] ?? $type;
@@ -321,6 +359,48 @@ final class AuditLogApiPresenter
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.');