feat: 更新 .env.example 文件,新增彩票业务配置与 Redis、邮件、队列等环境变量,优化开发环境设置

This commit is contained in:
2026-05-08 17:26:01 +08:00
parent 8cce1778b9
commit 85e57782cc
12 changed files with 663 additions and 54 deletions

View File

@@ -1,101 +1,206 @@
APP_NAME=Lottery # =============================================================================
APP_ENV=local # 应用核心config/app.php
APP_KEY= # =============================================================================
APP_DEBUG=true
APP_URL=http://localhost
# 应用显示名(邮件发件人名、日志等会引用)
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_LOCALE=en
# 翻译缺失时的回退语言
APP_FALLBACK_LOCALE=en APP_FALLBACK_LOCALE=en
# Faker 造数用的区域(如 zh_CN、en_US
APP_FAKER_LOCALE=en_US APP_FAKER_LOCALE=en_US
# =============================================================================
# 维护模式config/app.php
# =============================================================================
# 维护模式驱动file写 storage/framework/down或可使用其他存储
APP_MAINTENANCE_DRIVER=file APP_MAINTENANCE_DRIVER=file
# 使用 database 驱动维护状态时的存储配置(与 file 二选一场景下再取消注释)
# APP_MAINTENANCE_STORE=database # APP_MAINTENANCE_STORE=database
# 内置 PHP 开发服务器 worker 数量(多核本机可酌情打开)
# PHP_CLI_SERVER_WORKERS=4 # PHP_CLI_SERVER_WORKERS=4
# =============================================================================
# 密码哈希config/hashing.php
# =============================================================================
# bcrypt 迭代轮数;越大越慢越安全,测试环境可略低
BCRYPT_ROUNDS=12 BCRYPT_ROUNDS=12
# =============================================================================
# 日志config/logging.php
# =============================================================================
# 默认日志通道名,对应下方 LOG_STACK 中的通道之一
LOG_CHANNEL=stack LOG_CHANNEL=stack
# stack 通道所包含的子通道,逗号分隔,如 single、daily
LOG_STACK=single LOG_STACK=single
# 弃用警告单独输出到的通道null 表示忽略
LOG_DEPRECATIONS_CHANNEL=null LOG_DEPRECATIONS_CHANNEL=null
# 日志级别debug / info / notice / warning / error / critical / alert / emergency
LOG_LEVEL=debug LOG_LEVEL=debug
DB_CONNECTION=pgsql # =============================================================================
DB_HOST=127.0.0.1 # 数据库config/database.php
DB_PORT=5432 # =============================================================================
DB_DATABASE=lottery
DB_USERNAME=kang
DB_PASSWORD=123456
# 连接名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=
# =============================================================================
# Sessionconfig/session.php
# =============================================================================
# 会话驱动file / cookie / database / redis 等
SESSION_DRIVER=database SESSION_DRIVER=database
# 会话存活时间(分钟)
SESSION_LIFETIME=120 SESSION_LIFETIME=120
# 是否加密会话数据
SESSION_ENCRYPT=false SESSION_ENCRYPT=false
# Cookie 路径,一般根路径 /
SESSION_PATH=/ SESSION_PATH=/
# Cookie 域;单域本地开发常用 null
SESSION_DOMAIN=null SESSION_DOMAIN=null
BROADCAST_CONNECTION=log # =============================================================================
FILESYSTEM_DISK=local # 广播与文件config/broadcasting.php、config/filesystems.php
# 本地可用 database上线建议 redis与产品文档赔付池 Lua / 队列) # =============================================================================
QUEUE_CONNECTION=database
# 本地可用 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 CACHE_STORE=database
# 缓存键全局前缀;多环境共 Redis 时可用于隔离,一般可留空使用框架默认
# CACHE_PREFIX= # CACHE_PREFIX=
# =============================================================================
# Memcachedconfig/cache.php未用可保持默认
# =============================================================================
MEMCACHED_HOST=127.0.0.1 MEMCACHED_HOST=127.0.0.1
# =============================================================================
# Redisconfig/database.php 中 redis 连接)
# =============================================================================
# Redis 客户端扩展phpredis / predis
REDIS_CLIENT=phpredis REDIS_CLIENT=phpredis
REDIS_HOST=127.0.0.1 REDIS_HOST=127.0.0.1
# Redis 密码;无认证填 null
REDIS_PASSWORD=null REDIS_PASSWORD=null
REDIS_PORT=6379 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 MAIL_MAILER=log
# TLS 方案等smtp+URL 时用;一般用框架默认)
MAIL_SCHEME=null MAIL_SCHEME=null
MAIL_HOST=127.0.0.1 MAIL_HOST=127.0.0.1
MAIL_PORT=2525 MAIL_PORT=2525
MAIL_USERNAME=null MAIL_USERNAME=null
MAIL_PASSWORD=null MAIL_PASSWORD=null
# 默认发件人邮箱
MAIL_FROM_ADDRESS="hello@example.com" MAIL_FROM_ADDRESS="hello@example.com"
# 默认发件显示名(可引用 APP_NAME
MAIL_FROM_NAME="${APP_NAME}" MAIL_FROM_NAME="${APP_NAME}"
# =============================================================================
# AWS S3 等config/filesystems.php未用 S3 可留空)
# =============================================================================
# IAM 访问键 ID仅在使用 S3/队列等 AWS 能力时必填)
AWS_ACCESS_KEY_ID= AWS_ACCESS_KEY_ID=
# IAM Secret
AWS_SECRET_ACCESS_KEY= AWS_SECRET_ACCESS_KEY=
AWS_DEFAULT_REGION=us-east-1 AWS_DEFAULT_REGION=us-east-1
AWS_BUCKET= AWS_BUCKET=
# 兼容 MinIO 等路径风格 Endpoint 时使用 true
AWS_USE_PATH_STYLE_ENDPOINT=false AWS_USE_PATH_STYLE_ENDPOINT=false
# =============================================================================
# 前端 Vitevite 构建时注入)
# =============================================================================
# 供前端读取的应用名,通常与 APP_NAME 一致
VITE_APP_NAME="${APP_NAME}" VITE_APP_NAME="${APP_NAME}"
# --------------------------------------------------------------------------- # =============================================================================
# 彩票业务(按对接填写;勿把真实密钥提交到 Git只在本地 .env # 彩票业务(config/lottery.php、database/seeders密钥仅写本机 .env
# --------------------------------------------------------------------------- # =============================================================================
# 默认结算币种(PRDNPR # 默认结算币种(产品约定,如 NPR
LOTTERY_DEFAULT_CURRENCY=NPR LOTTERY_DEFAULT_CURRENCY=NPR
# lottery_settings 表读缓存 TTL调小更易立即看到后台改值调大减库压
# 本地开发Bearer dev:{数据库 players.id}(仅 APP_ENV=local 且为 true 时生效) LOTTERY_SETTINGS_CACHE_TTL=60
# 本地开发Authorization: Bearer dev:{players.id};仅 APP_ENV=local 且为 true 时生效,生产务必 false
LOTTERY_PLAYER_AUTH_DEV_BYPASS=false LOTTERY_PLAYER_AUTH_DEV_BYPASS=false
# db:seed — 管理员种子为 admin@admin.com / 123456非 production勿用于生产库 # 校验主站 JWT 的算法(与签发方一致)
# db:seed — 演示玩家钱包余额(最小货币单位整数) LOTTERY_JWT_ALGORITHM=HS256
# DEV_SEED_WALLET_BALANCE_MINOR=125000 # JWT 内表示站点编码的 claim 名
# DEV_SEED_WALLET_FROZEN_MINOR=0 LOTTERY_JWT_CLAIM_SITE_CODE=site_code
# JWT 内表示主站玩家标识的 claim 名
LOTTERY_JWT_CLAIM_SITE_PLAYER_ID=site_player_id
# JWT 内站点/玩家字段名(与主站签发约定一致 # 主站站点根 URLSSO、跳转等
# LOTTERY_JWT_ALGORITHM=HS256 MAIN_SITE_BASE_URL=
# LOTTERY_JWT_CLAIM_SITE_CODE=site_code # 主站 JWT 验签密钥(与主站约定,勿泄露)
# LOTTERY_JWT_CLAIM_SITE_PLAYER_ID=site_player_id MAIN_SITE_SSO_JWT_SECRET=
# 主站钱包接口基地址
MAIN_SITE_WALLET_API_URL=
# 主站钱包接口访问密钥
MAIN_SITE_WALLET_API_KEY=
# 主站钱包 HTTP 超时(秒)
MAIN_SITE_WALLET_TIMEOUT=10
# 主站 SSO / 钱包(名称可按实际接口调整 # db:seed 演示玩家钱包:可用余额(最小货币单位,整数
# MAIN_SITE_BASE_URL= DEV_SEED_WALLET_BALANCE_MINOR=125000
# MAIN_SITE_SSO_JWT_SECRET= # db:seed 演示玩家钱包:冻结余额(最小货币单位,整数)
# MAIN_SITE_WALLET_API_URL= DEV_SEED_WALLET_FROZEN_MINOR=0
# MAIN_SITE_WALLET_API_KEY=
# MAIN_SITE_WALLET_TIMEOUT=10
# SanctumH5 / Next 管理端与 API 不同端口时需配置可识别的前端域名 # Sanctum SPA 场景:与 API 不同端口的前端域名列表,逗号分隔,用于有状态 Cookie 鉴权
# SANCTUM_STATEFUL_DOMAINS=localhost,localhost:3000,127.0.0.1 SANCTUM_STATEFUL_DOMAINS=
# Redis 库划分(可选,与 CACHE / SESSION 分开时用)
# REDIS_DB=0
# REDIS_CACHE_DB=1

38
app/Models/AuditLog.php Normal file
View File

@@ -0,0 +1,38 @@
<?php
namespace App\Models;
use App\Services\AuditLogger;
use Illuminate\Database\Eloquent\Model;
/** 运维/后台动作留痕表 audit_logs写入请走 {@see AuditLogger})。 */
class AuditLog extends Model
{
public const UPDATED_AT = null;
protected $table = 'audit_logs';
/** @var list<string> */
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',
];
}
}

View File

@@ -0,0 +1,25 @@
<?php
namespace App\Models;
use App\Services\LotterySettings;
use Illuminate\Database\Eloquent\Model;
/** 运行期 KV 配置行,对应表 lottery_settings读业务请优先用 {@see LotterySettings})。 */
class LotterySetting extends Model
{
protected $fillable = [
'setting_key',
'value_json',
'group_name',
'description_zh',
];
protected function casts(): array
{
return [
/** 支持 JSON 布尔/数字/字符串/对象任意顶层形态 */
'value_json' => 'json',
];
}
}

View File

@@ -0,0 +1,166 @@
<?php
namespace App\Services;
use App\Models\AdminUser;
use App\Models\AuditLog;
use App\Models\Player;
use Illuminate\Http\Request;
/**
* 审计日志写入入口:落到表 audit_logs created_at。
*
* operator_typeadmin / player / system常量见此类
*/
final class AuditLogger
{
/** 后台账号operator_id = admin_users.id */
public const OPERATOR_ADMIN = 'admin';
/** 终端玩家operator_id = players.id */
public const OPERATOR_PLAYER = 'player';
/** 系统任务(定时任务、异步 Job 无自然人操作者时使用) */
public const OPERATOR_SYSTEM = 'system';
/**
* @param array<string, mixed>|null $beforeJson
* @param array<string, mixed>|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<string, mixed>|null $beforeJson
* @param array<string, mixed>|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);
}
}

View File

@@ -0,0 +1,94 @@
<?php
namespace App\Services;
use App\Models\LotterySetting;
use Illuminate\Support\Facades\Cache;
/**
* 【配置中心】统一读入口:按 key 取运行期配置,默认带缓存。
*
* - 写入:控制台 / Seeder / 后续管理端可调 {@see put()}
* - 【重要】库里不存在的 key **不写入缓存**,避免长期命中默认值导致后台新增配置不生效。
*/
final class LotterySettings
{
public static function cacheTtlSeconds(): int
{
return max(5, (int) config('lottery.settings.cache_ttl_seconds', 60));
}
/** 取单个配置;若无行则返回 $default不写缓存。 */
public static function get(string $key, mixed $default = null): mixed
{
$cacheKey = self::cacheKey($key);
if (Cache::has($cacheKey)) {
return Cache::get($cacheKey);
}
/** @var LotterySetting|null $row */
$row = LotterySetting::query()->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<string, mixed> $keysToDefault key => default
* @return array<string, mixed>
*/
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;
}
}

View File

@@ -17,6 +17,13 @@ return [
'default_currency' => env('LOTTERY_DEFAULT_CURRENCY', 'NPR'), 'default_currency' => env('LOTTERY_DEFAULT_CURRENCY', 'NPR'),
/*
| lottery_settings 表读缓存 TTL。调小可更快看到后台改值调大减 DB 压力。
*/
'settings' => [
'cache_ttl_seconds' => (int) env('LOTTERY_SETTINGS_CACHE_TTL', 60),
],
'main_site' => [ 'main_site' => [
'base_url' => env('MAIN_SITE_BASE_URL'), 'base_url' => env('MAIN_SITE_BASE_URL'),
'sso_jwt_secret' => env('MAIN_SITE_SSO_JWT_SECRET'), 'sso_jwt_secret' => env('MAIN_SITE_SSO_JWT_SECRET'),

View File

@@ -0,0 +1,34 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
/**
* 【任务 3 · 配置中心】键值运行时配置。
*
* `config/*.php` / `.env` 分工:
* - 密钥、站点 URL 等仍以 env 为准,不进库或不在此表明文扩写密钥;
* - 可随时由运营调整的开关、限额类配置放本表,读时走 LotterySettings 带缓存。
*/
return new class extends Migration
{
public function up(): void
{
Schema::create('lottery_settings', function (Blueprint $table) {
$table->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');
}
};

View File

@@ -15,6 +15,7 @@ class DatabaseSeeder extends Seeder
$this->call([ $this->call([
CurrencySeeder::class, CurrencySeeder::class,
PlayTypeSeeder::class, PlayTypeSeeder::class,
LotterySettingsSeeder::class,
]); ]);
// 演示管理员 + 演示玩家:**勿在生产库执行**(或确保 APP_ENV≠production // 演示管理员 + 演示玩家:**勿在生产库执行**(或确保 APP_ENV≠production

View File

@@ -0,0 +1,36 @@
<?php
namespace Database\Seeders;
use App\Services\LotterySettings;
use Illuminate\Database\Seeder;
/**
* 【配置中心】示例键;后续由运营后台维护同表即可。
*/
class LotterySettingsSeeder extends Seeder
{
public function run(): void
{
LotterySettings::put(
'wallet.transfer_in_enabled',
true,
'wallet',
'是否允许玩家端发起转入(主站对接前可关)',
);
LotterySettings::put(
'wallet.transfer_out_enabled',
true,
'wallet',
'是否允许玩家端发起转出',
);
LotterySettings::put(
'app.display_name_for_client',
'Lottery',
'general',
'客户端展示用短名称(示例)',
);
}
}

View File

@@ -8,47 +8,41 @@ use App\Http\Controllers\Api\V1\Wallet\WalletBalanceController;
use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Route;
/* /*
| Laravel bootstrap 为本文件自动加前缀 `api` + 中间件组 `api`,故实际 URL | Laravel 为本文件自动加前缀 `api`,此处再写 `v1`,故完整路径形如 `/api/v1/health`
| /api/v1/health
| /api/v1/player/...
| /api/v1/wallet/...
| /api/v1/admin/...
*/ */
Route::prefix('v1')->group(function (): void { Route::prefix('v1')->group(function (): void {
// 探活:无鉴权 // 名称:服务健康检查
Route::get('health', HealthController::class)->name('api.v1.health'); Route::get('health', HealthController::class)->name('api.v1.health');
// 玩家前缀下:仅 ping 公开me 与 wallet/* 共用 lottery.player见下方大组
Route::prefix('player') Route::prefix('player')
->name('api.v1.player.') ->name('api.v1.player.')
->group(function (): void { ->group(function (): void {
// 名称:玩家端连通性探测
Route::get('ping', PlayerPingController::class)->name('ping'); 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::middleware('lottery.player')->group(function (): void {
Route::prefix('player') Route::prefix('player')
->name('api.v1.player.') ->name('api.v1.player.')
->group(function (): void { ->group(function (): void {
// 名称:当前登录玩家信息
Route::get('me', MeController::class)->name('me'); Route::get('me', MeController::class)->name('me');
}); });
Route::prefix('wallet') Route::prefix('wallet')
->name('api.v1.wallet.') ->name('api.v1.wallet.')
->group(function (): void { ->group(function (): void {
// 名称:彩票钱包余额查询
Route::get('balance', WalletBalanceController::class)->name('balance'); Route::get('balance', WalletBalanceController::class)->name('balance');
}); });
}); });
// 后台 APIlottery.admin 内预留 Sanctum / RBAC
Route::middleware('lottery.admin') Route::middleware('lottery.admin')
->prefix('admin') ->prefix('admin')
->name('api.v1.admin.') ->name('api.v1.admin.')
->group(function (): void { ->group(function (): void {
// 名称:后台接口连通性探测
Route::get('ping', AdminPingController::class)->name('ping'); Route::get('ping', AdminPingController::class)->name('ping');
}); });
}); });

View File

@@ -0,0 +1,79 @@
<?php
use App\Models\AuditLog;
use App\Services\AuditLogger;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Http\Request;
uses(RefreshDatabase::class);
test('audit logger persists row with snapshots', function (): void {
$row = AuditLogger::record(
AuditLogger::OPERATOR_ADMIN,
42,
'settings',
'update',
'lottery_setting',
'deposit_min',
['value_json' => 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);
});

View File

@@ -0,0 +1,30 @@
<?php
use App\Models\LotterySetting;
use App\Services\LotterySettings;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Cache;
uses(RefreshDatabase::class);
test('lottery settings get returns seeded value', function (): void {
LotterySettings::put('fixture.flag', true, 'test', null);
expect(LotterySettings::get('fixture.flag'))->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');
});