From 85e57782cc8024a5732c41024f70b7d830f4fdbe Mon Sep 17 00:00:00 2001 From: kang Date: Fri, 8 May 2026 17:26:01 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=9B=B4=E6=96=B0=20.env.example=20?= =?UTF-8?q?=E6=96=87=E4=BB=B6=EF=BC=8C=E6=96=B0=E5=A2=9E=E5=BD=A9=E7=A5=A8?= =?UTF-8?q?=E4=B8=9A=E5=8A=A1=E9=85=8D=E7=BD=AE=E4=B8=8E=20Redis=E3=80=81?= =?UTF-8?q?=E9=82=AE=E4=BB=B6=E3=80=81=E9=98=9F=E5=88=97=E7=AD=89=E7=8E=AF?= =?UTF-8?q?=E5=A2=83=E5=8F=98=E9=87=8F=EF=BC=8C=E4=BC=98=E5=8C=96=E5=BC=80?= =?UTF-8?q?=E5=8F=91=E7=8E=AF=E5=A2=83=E8=AE=BE=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env.example | 189 ++++++++++++++---- app/Models/AuditLog.php | 38 ++++ app/Models/LotterySetting.php | 25 +++ app/Services/AuditLogger.php | 166 +++++++++++++++ app/Services/LotterySettings.php | 94 +++++++++ config/lottery.php | 7 + ...8_140000_create_lottery_settings_table.php | 34 ++++ database/seeders/DatabaseSeeder.php | 1 + database/seeders/LotterySettingsSeeder.php | 36 ++++ routes/api.php | 18 +- tests/Feature/AuditLoggerTest.php | 79 ++++++++ tests/Feature/LotterySettingsTest.php | 30 +++ 12 files changed, 663 insertions(+), 54 deletions(-) create mode 100644 app/Models/AuditLog.php create mode 100644 app/Models/LotterySetting.php create mode 100644 app/Services/AuditLogger.php create mode 100644 app/Services/LotterySettings.php create mode 100644 database/migrations/2026_05_08_140000_create_lottery_settings_table.php create mode 100644 database/seeders/LotterySettingsSeeder.php create mode 100644 tests/Feature/AuditLoggerTest.php create mode 100644 tests/Feature/LotterySettingsTest.php diff --git a/.env.example b/.env.example index 649f6c0..2c9fef1 100644 --- a/.env.example +++ b/.env.example @@ -1,101 +1,206 @@ -APP_NAME=Lottery -APP_ENV=local -APP_KEY= -APP_DEBUG=true -APP_URL=http://localhost +# ============================================================================= +# 应用核心(config/app.php) +# ============================================================================= +# 应用显示名(邮件发件人名、日志等会引用) +APP_NAME=Lottery +# 运行环境:local / staging / production(影响缓存、调试与安全策略) +APP_ENV=local +# 应用密钥;留空时执行 php artisan key:generate 生成(勿提交真实值到 Git) +APP_KEY= +# 是否输出详细错误页与堆栈(生产环境务必为 false) +APP_DEBUG=true +# 应用根 URL(生成链接、邮件、部分驱动依赖;与 php artisan serve 端口一致时带上 :8000) +APP_URL=http://localhost:8000 + +# ============================================================================= +# 语言与假数据(config/app.php) +# ============================================================================= + +# PHP 应用默认语言代码 APP_LOCALE=en +# 翻译缺失时的回退语言 APP_FALLBACK_LOCALE=en +# Faker 造数用的区域(如 zh_CN、en_US) APP_FAKER_LOCALE=en_US +# ============================================================================= +# 维护模式(config/app.php) +# ============================================================================= + +# 维护模式驱动:file(写 storage/framework/down)或可使用其他存储 APP_MAINTENANCE_DRIVER=file +# 使用 database 驱动维护状态时的存储配置(与 file 二选一场景下再取消注释) # APP_MAINTENANCE_STORE=database +# 内置 PHP 开发服务器 worker 数量(多核本机可酌情打开) # PHP_CLI_SERVER_WORKERS=4 +# ============================================================================= +# 密码哈希(config/hashing.php) +# ============================================================================= + +# bcrypt 迭代轮数;越大越慢越安全,测试环境可略低 BCRYPT_ROUNDS=12 +# ============================================================================= +# 日志(config/logging.php) +# ============================================================================= + +# 默认日志通道名,对应下方 LOG_STACK 中的通道之一 LOG_CHANNEL=stack +# stack 通道所包含的子通道,逗号分隔,如 single、daily LOG_STACK=single +# 弃用警告单独输出到的通道;null 表示忽略 LOG_DEPRECATIONS_CHANNEL=null +# 日志级别:debug / info / notice / warning / error / critical / alert / emergency LOG_LEVEL=debug -DB_CONNECTION=pgsql -DB_HOST=127.0.0.1 -DB_PORT=5432 -DB_DATABASE=lottery -DB_USERNAME=kang -DB_PASSWORD=123456 +# ============================================================================= +# 数据库(config/database.php) +# ============================================================================= +# 连接名:pgsql / mysql / sqlite 等 +DB_CONNECTION=pgsql +# 数据库主机 +DB_HOST=127.0.0.1 +# 数据库端口(PostgreSQL 默认 5432) +DB_PORT=5432 +# 数据库名 +DB_DATABASE=lottery +# 数据库用户名(示例留空,本机按实际填写) +DB_USERNAME= +# 数据库密码(示例留空) +DB_PASSWORD= +# 完整数据库 URL,若设置可覆盖上述分散配置(一般留空) +# DB_URL= + +# ============================================================================= +# Session(config/session.php) +# ============================================================================= + +# 会话驱动:file / cookie / database / redis 等 SESSION_DRIVER=database +# 会话存活时间(分钟) SESSION_LIFETIME=120 +# 是否加密会话数据 SESSION_ENCRYPT=false +# Cookie 路径,一般根路径 / SESSION_PATH=/ +# Cookie 域;单域本地开发常用 null SESSION_DOMAIN=null -BROADCAST_CONNECTION=log -FILESYSTEM_DISK=local -# 本地可用 database;上线建议 redis(与产品文档:赔付池 Lua / 队列) -QUEUE_CONNECTION=database +# ============================================================================= +# 广播与文件(config/broadcasting.php、config/filesystems.php) +# ============================================================================= -# 本地可用 database;实现赔付池与热点缓存后改为 redis +# 广播驱动:log / pusher / redis 等(未用实时广播可保持 log) +BROADCAST_CONNECTION=log +# 默认文件存储盘:local / s3 等 +FILESYSTEM_DISK=local + +# ============================================================================= +# 队列与缓存(config/queue.php、config/cache.php) +# ============================================================================= + +# 队列驱动:sync(同步)/ database / redis 等;本地常用 database +QUEUE_CONNECTION=database +# 缓存存储:file / database / redis 等;与 Redis 赔付池等能力对接前可用 database CACHE_STORE=database +# 缓存键全局前缀;多环境共 Redis 时可用于隔离,一般可留空使用框架默认 # CACHE_PREFIX= +# ============================================================================= +# Memcached(config/cache.php;未用可保持默认) +# ============================================================================= + MEMCACHED_HOST=127.0.0.1 +# ============================================================================= +# Redis(config/database.php 中 redis 连接) +# ============================================================================= + +# Redis 客户端扩展:phpredis / predis REDIS_CLIENT=phpredis REDIS_HOST=127.0.0.1 +# Redis 密码;无认证填 null REDIS_PASSWORD=null REDIS_PORT=6379 +# 默认 Redis 逻辑库编号(与 session/queue 共用连接时注意规划) +REDIS_DB=0 +# 专用于 cache 连接的逻辑库编号(config 中 default 与 cache 两套连接) +REDIS_CACHE_DB=1 +# ============================================================================= +# 邮件(config/mail.php) +# ============================================================================= + +# 邮件驱动:smtp / log(仅写日志)等 MAIL_MAILER=log +# TLS 方案等(smtp+URL 时用;一般用框架默认) MAIL_SCHEME=null MAIL_HOST=127.0.0.1 MAIL_PORT=2525 MAIL_USERNAME=null MAIL_PASSWORD=null +# 默认发件人邮箱 MAIL_FROM_ADDRESS="hello@example.com" +# 默认发件显示名(可引用 APP_NAME) MAIL_FROM_NAME="${APP_NAME}" +# ============================================================================= +# AWS S3 等(config/filesystems.php;未用 S3 可留空) +# ============================================================================= + +# IAM 访问键 ID(仅在使用 S3/队列等 AWS 能力时必填) AWS_ACCESS_KEY_ID= +# IAM Secret AWS_SECRET_ACCESS_KEY= AWS_DEFAULT_REGION=us-east-1 AWS_BUCKET= +# 兼容 MinIO 等路径风格 Endpoint 时使用 true AWS_USE_PATH_STYLE_ENDPOINT=false +# ============================================================================= +# 前端 Vite(vite 构建时注入) +# ============================================================================= + +# 供前端读取的应用名,通常与 APP_NAME 一致 VITE_APP_NAME="${APP_NAME}" -# --------------------------------------------------------------------------- -# 彩票业务(按对接填写;勿把真实密钥提交到 Git,只在本地 .env 写) -# --------------------------------------------------------------------------- +# ============================================================================= +# 彩票业务(config/lottery.php、database/seeders;密钥仅写本机 .env) +# ============================================================================= -# 默认结算币种(PRD:NPR) +# 默认结算币种(产品约定,如 NPR) LOTTERY_DEFAULT_CURRENCY=NPR - -# 本地开发:Bearer dev:{数据库 players.id}(仅 APP_ENV=local 且为 true 时生效) +# lottery_settings 表读缓存 TTL(秒);调小更易立即看到后台改值,调大减库压 +LOTTERY_SETTINGS_CACHE_TTL=60 +# 本地开发:Authorization: Bearer dev:{players.id};仅 APP_ENV=local 且为 true 时生效,生产务必 false LOTTERY_PLAYER_AUTH_DEV_BYPASS=false -# db:seed — 管理员种子为 admin@admin.com / 123456(非 production);勿用于生产库 -# db:seed — 演示玩家钱包余额(最小货币单位整数) -# DEV_SEED_WALLET_BALANCE_MINOR=125000 -# DEV_SEED_WALLET_FROZEN_MINOR=0 +# 校验主站 JWT 的算法(与签发方一致) +LOTTERY_JWT_ALGORITHM=HS256 +# JWT 内表示站点编码的 claim 名 +LOTTERY_JWT_CLAIM_SITE_CODE=site_code +# JWT 内表示主站玩家标识的 claim 名 +LOTTERY_JWT_CLAIM_SITE_PLAYER_ID=site_player_id -# JWT 内站点/玩家字段名(与主站签发约定一致) -# LOTTERY_JWT_ALGORITHM=HS256 -# LOTTERY_JWT_CLAIM_SITE_CODE=site_code -# LOTTERY_JWT_CLAIM_SITE_PLAYER_ID=site_player_id +# 主站站点根 URL(SSO、跳转等) +MAIN_SITE_BASE_URL= +# 主站 JWT 验签密钥(与主站约定,勿泄露) +MAIN_SITE_SSO_JWT_SECRET= +# 主站钱包接口基地址 +MAIN_SITE_WALLET_API_URL= +# 主站钱包接口访问密钥 +MAIN_SITE_WALLET_API_KEY= +# 主站钱包 HTTP 超时(秒) +MAIN_SITE_WALLET_TIMEOUT=10 -# 主站 SSO / 钱包(名称可按实际接口调整) -# MAIN_SITE_BASE_URL= -# MAIN_SITE_SSO_JWT_SECRET= -# MAIN_SITE_WALLET_API_URL= -# MAIN_SITE_WALLET_API_KEY= -# MAIN_SITE_WALLET_TIMEOUT=10 +# db:seed 演示玩家钱包:可用余额(最小货币单位,整数) +DEV_SEED_WALLET_BALANCE_MINOR=125000 +# db:seed 演示玩家钱包:冻结余额(最小货币单位,整数) +DEV_SEED_WALLET_FROZEN_MINOR=0 -# Sanctum:H5 / Next 管理端与 API 不同端口时需配置可识别的前端域名 -# SANCTUM_STATEFUL_DOMAINS=localhost,localhost:3000,127.0.0.1 - -# Redis 库划分(可选,与 CACHE / SESSION 分开时用) -# REDIS_DB=0 -# REDIS_CACHE_DB=1 +# Sanctum SPA 场景:与 API 不同端口的前端域名列表,逗号分隔,用于有状态 Cookie 鉴权 +SANCTUM_STATEFUL_DOMAINS= diff --git a/app/Models/AuditLog.php b/app/Models/AuditLog.php new file mode 100644 index 0000000..ecb0c93 --- /dev/null +++ b/app/Models/AuditLog.php @@ -0,0 +1,38 @@ + */ + protected $fillable = [ + 'operator_type', + 'operator_id', + 'module_code', + 'action_code', + 'target_type', + 'target_id', + 'before_json', + 'after_json', + 'ip', + 'user_agent', + ]; + + protected function casts(): array + { + return [ + 'operator_id' => 'integer', + 'before_json' => 'json', + 'after_json' => 'json', + 'created_at' => 'datetime', + ]; + } +} diff --git a/app/Models/LotterySetting.php b/app/Models/LotterySetting.php new file mode 100644 index 0000000..4c456d6 --- /dev/null +++ b/app/Models/LotterySetting.php @@ -0,0 +1,25 @@ + 'json', + ]; + } +} diff --git a/app/Services/AuditLogger.php b/app/Services/AuditLogger.php new file mode 100644 index 0000000..bbdeb96 --- /dev/null +++ b/app/Services/AuditLogger.php @@ -0,0 +1,166 @@ +|null $beforeJson + * @param array|null $afterJson + */ + public static function record( + string $operatorType, + int $operatorId, + ?string $moduleCode = null, + ?string $actionCode = null, + ?string $targetType = null, + ?string $targetId = null, + ?array $beforeJson = null, + ?array $afterJson = null, + ?string $ip = null, + ?string $userAgent = null, + ): AuditLog { + return AuditLog::query()->create([ + 'operator_type' => $operatorType, + 'operator_id' => $operatorId, + 'module_code' => $moduleCode, + 'action_code' => $actionCode, + 'target_type' => $targetType, + 'target_id' => $targetId, + 'before_json' => $beforeJson, + 'after_json' => $afterJson, + 'ip' => $ip, + 'user_agent' => self::truncateUserAgent($userAgent), + ]); + } + + /** + * 从当前 HTTP 请求补全 IP、User-Agent(后台 / 玩家 API 调用方便)。 + * + * @param array|null $beforeJson + * @param array|null $afterJson + */ + public static function recordFromRequest( + Request $request, + string $operatorType, + int $operatorId, + ?string $moduleCode = null, + ?string $actionCode = null, + ?string $targetType = null, + ?string $targetId = null, + ?array $beforeJson = null, + ?array $afterJson = null, + ): AuditLog { + return self::record( + $operatorType, + $operatorId, + $moduleCode, + $actionCode, + $targetType, + $targetId, + $beforeJson, + $afterJson, + $request->ip(), + $request->userAgent(), + ); + } + + public static function recordForAdmin(AdminUser $admin, ?Request $request = null, ?string $moduleCode = null, ?string $actionCode = null, ?string $targetType = null, ?string $targetId = null, ?array $beforeJson = null, ?array $afterJson = null): AuditLog + { + if ($request !== null) { + return self::recordFromRequest( + $request, + self::OPERATOR_ADMIN, + (int) $admin->getKey(), + $moduleCode, + $actionCode, + $targetType, + $targetId, + $beforeJson, + $afterJson, + ); + } + + return self::record( + self::OPERATOR_ADMIN, + (int) $admin->getKey(), + $moduleCode, + $actionCode, + $targetType, + $targetId, + $beforeJson, + $afterJson, + ); + } + + public static function recordForPlayer(Player $player, ?Request $request = null, ?string $moduleCode = null, ?string $actionCode = null, ?string $targetType = null, ?string $targetId = null, ?array $beforeJson = null, ?array $afterJson = null): AuditLog + { + if ($request !== null) { + return self::recordFromRequest( + $request, + self::OPERATOR_PLAYER, + (int) $player->getKey(), + $moduleCode, + $actionCode, + $targetType, + $targetId, + $beforeJson, + $afterJson, + ); + } + + return self::record( + self::OPERATOR_PLAYER, + (int) $player->getKey(), + $moduleCode, + $actionCode, + $targetType, + $targetId, + $beforeJson, + $afterJson, + ); + } + + /** 定时任务或队列内无 Request 时使用。 */ + public static function recordForSystem(?string $moduleCode = null, ?string $actionCode = null, ?string $targetType = null, ?string $targetId = null, ?array $beforeJson = null, ?array $afterJson = null): AuditLog + { + return self::record( + self::OPERATOR_SYSTEM, + 0, + $moduleCode, + $actionCode, + $targetType, + $targetId, + $beforeJson, + $afterJson, + ); + } + + private static function truncateUserAgent(?string $userAgent): ?string + { + if ($userAgent === null || $userAgent === '') { + return null; + } + + return mb_substr($userAgent, 0, 255); + } +} diff --git a/app/Services/LotterySettings.php b/app/Services/LotterySettings.php new file mode 100644 index 0000000..4ad43d8 --- /dev/null +++ b/app/Services/LotterySettings.php @@ -0,0 +1,94 @@ +where('setting_key', $key)->first(); + if ($row === null) { + return $default; + } + + $value = self::normalizeValue($row->value_json); + Cache::put($cacheKey, $value, self::cacheTtlSeconds()); + + return $value; + } + + /** + * 批量读取(逐项走 get;已存在的键会受益于缓存)。 + * + * @param array $keysToDefault key => default + * @return array + */ + public static function many(array $keysToDefault): array + { + $out = []; + foreach ($keysToDefault as $key => $def) { + $out[$key] = self::get($key, $def); + } + + return $out; + } + + /** 删单 key 缓存;写入 lottery_settings 后务必调用(或运维 `php artisan cache:clear`) */ + public static function forgetKey(string $key): void + { + Cache::forget(self::cacheKey($key)); + } + + /** + * 写入或更新一行配置并刷新该 key 的缓存。(Seeder / 后续管理端共用) + */ + public static function put( + string $key, + mixed $value, + string $groupName = 'general', + ?string $descriptionZh = null, + ): void { + LotterySetting::query()->updateOrCreate( + ['setting_key' => $key], + [ + 'value_json' => $value, + 'group_name' => $groupName, + 'description_zh' => $descriptionZh, + ], + ); + self::forgetKey($key); + // 写入后立即预热缓存,下一次 get 可走 Cache::has + Cache::put(self::cacheKey($key), self::normalizeValue($value), self::cacheTtlSeconds()); + } + + public static function cacheKey(string $key): string + { + return 'lottery_settings:'.$key; + } + + private static function normalizeValue(mixed $value): mixed + { + return $value; + } +} diff --git a/config/lottery.php b/config/lottery.php index 853625c..b5aa911 100644 --- a/config/lottery.php +++ b/config/lottery.php @@ -17,6 +17,13 @@ return [ 'default_currency' => env('LOTTERY_DEFAULT_CURRENCY', 'NPR'), + /* + | lottery_settings 表读缓存 TTL(秒)。调小可更快看到后台改值,调大减 DB 压力。 + */ + 'settings' => [ + 'cache_ttl_seconds' => (int) env('LOTTERY_SETTINGS_CACHE_TTL', 60), + ], + 'main_site' => [ 'base_url' => env('MAIN_SITE_BASE_URL'), 'sso_jwt_secret' => env('MAIN_SITE_SSO_JWT_SECRET'), diff --git a/database/migrations/2026_05_08_140000_create_lottery_settings_table.php b/database/migrations/2026_05_08_140000_create_lottery_settings_table.php new file mode 100644 index 0000000..156350a --- /dev/null +++ b/database/migrations/2026_05_08_140000_create_lottery_settings_table.php @@ -0,0 +1,34 @@ +id(); + $table->string('setting_key', 160)->unique(); + $table->json('value_json'); + $table->string('group_name', 64)->default('general')->comment('控制台分组展示用'); + $table->string('description_zh')->nullable()->comment('运维说明'); + $table->timestamps(); + + $table->index('group_name', 'idx_lottery_settings_group'); + }); + } + + public function down(): void + { + Schema::dropIfExists('lottery_settings'); + } +}; diff --git a/database/seeders/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php index f195525..c44627c 100644 --- a/database/seeders/DatabaseSeeder.php +++ b/database/seeders/DatabaseSeeder.php @@ -15,6 +15,7 @@ class DatabaseSeeder extends Seeder $this->call([ CurrencySeeder::class, PlayTypeSeeder::class, + LotterySettingsSeeder::class, ]); // 演示管理员 + 演示玩家:**勿在生产库执行**(或确保 APP_ENV≠production) diff --git a/database/seeders/LotterySettingsSeeder.php b/database/seeders/LotterySettingsSeeder.php new file mode 100644 index 0000000..f138371 --- /dev/null +++ b/database/seeders/LotterySettingsSeeder.php @@ -0,0 +1,36 @@ +group(function (): void { - // 探活:无鉴权 + // 名称:服务健康检查 Route::get('health', HealthController::class)->name('api.v1.health'); - // 玩家前缀下:仅 ping 公开;me 与 wallet/* 共用 lottery.player(见下方大组) Route::prefix('player') ->name('api.v1.player.') ->group(function (): void { + // 名称:玩家端连通性探测 Route::get('ping', PlayerPingController::class)->name('ping'); }); - /* - | 已登录玩家:PRD 把路径拆成 `/v1/player/*` 与 `/v1/wallet/*`,这里用同一中间件块避免重复写。 - | 勿把 wallet 挂到 player 前缀下,否则会变成 /api/v1/player/wallet/...,与 PRD §10.1.1 不一致。 - */ Route::middleware('lottery.player')->group(function (): void { Route::prefix('player') ->name('api.v1.player.') ->group(function (): void { + // 名称:当前登录玩家信息 Route::get('me', MeController::class)->name('me'); }); Route::prefix('wallet') ->name('api.v1.wallet.') ->group(function (): void { + // 名称:彩票钱包余额查询 Route::get('balance', WalletBalanceController::class)->name('balance'); }); }); - // 后台 API;lottery.admin 内预留 Sanctum / RBAC Route::middleware('lottery.admin') ->prefix('admin') ->name('api.v1.admin.') ->group(function (): void { + // 名称:后台接口连通性探测 Route::get('ping', AdminPingController::class)->name('ping'); }); }); diff --git a/tests/Feature/AuditLoggerTest.php b/tests/Feature/AuditLoggerTest.php new file mode 100644 index 0000000..616e462 --- /dev/null +++ b/tests/Feature/AuditLoggerTest.php @@ -0,0 +1,79 @@ + 100], + ['value_json' => 200], + '203.0.113.10', + 'PHPUnit', + ); + + expect($row)->toBeInstanceOf(AuditLog::class) + ->operator_type->toBe(AuditLogger::OPERATOR_ADMIN) + ->operator_id->toBe(42) + ->module_code->toBe('settings') + ->action_code->toBe('update') + ->target_type->toBe('lottery_setting') + ->target_id->toBe('deposit_min') + ->before_json->toBe(['value_json' => 100]) + ->after_json->toBe(['value_json' => 200]) + ->ip->toBe('203.0.113.10') + ->user_agent->toBe('PHPUnit'); + + expect(AuditLog::query()->count())->toBe(1); +}); + +test('audit logger truncates user agent to 255', function (): void { + $ua = str_repeat('a', 300); + + $row = AuditLogger::record(AuditLogger::OPERATOR_SYSTEM, 0, 'job', 'run', ip: null, userAgent: $ua); + + expect(strlen((string) $row->user_agent))->toBe(255); +}); + +test('audit logger record from request fills ip', function (): void { + $request = Request::create('/fake', 'POST', server: [ + 'REMOTE_ADDR' => '198.51.100.2', + 'HTTP_USER_AGENT' => 'TestAgent', + ]); + + $row = AuditLogger::recordFromRequest( + $request, + AuditLogger::OPERATOR_PLAYER, + 7, + 'wallet', + 'transfer', + null, + null, + ['x' => 1], + ['x' => 2], + ); + + expect($row) + ->ip->toContain('198.51.100.2') + ->user_agent->toBe('TestAgent'); +}); + +test('record for system uses operator zero', function (): void { + AuditLogger::recordForSystem('reconcile', 'start', targetId: '2026-01'); + + /** @var AuditLog|null $row */ + $row = AuditLog::query()->latest('id')->first(); + + expect($row)->not->toBeNull() + ->operator_type->toBe(AuditLogger::OPERATOR_SYSTEM) + ->operator_id->toBe(0); +}); diff --git a/tests/Feature/LotterySettingsTest.php b/tests/Feature/LotterySettingsTest.php new file mode 100644 index 0000000..4bca554 --- /dev/null +++ b/tests/Feature/LotterySettingsTest.php @@ -0,0 +1,30 @@ +toBeTrue(); +}); + +test('lottery settings misses are not cached', function (): void { + Cache::flush(); + + expect(LotterySettings::get('not.exists', 'fallback'))->toBe('fallback'); + + LotterySetting::query()->create([ + 'setting_key' => 'not.exists', + 'value_json' => 'hello', + 'group_name' => 'test', + ]); + + LotterySettings::forgetKey('not.exists'); + + expect(LotterySettings::get('not.exists'))->toBe('hello'); +});