feat: 更新 .env.example 文件,新增彩票业务配置与 Redis、邮件、队列等环境变量,优化开发环境设置
This commit is contained in:
189
.env.example
189
.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=
|
||||
|
||||
38
app/Models/AuditLog.php
Normal file
38
app/Models/AuditLog.php
Normal 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',
|
||||
];
|
||||
}
|
||||
}
|
||||
25
app/Models/LotterySetting.php
Normal file
25
app/Models/LotterySetting.php
Normal 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',
|
||||
];
|
||||
}
|
||||
}
|
||||
166
app/Services/AuditLogger.php
Normal file
166
app/Services/AuditLogger.php
Normal 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_type:admin / 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);
|
||||
}
|
||||
}
|
||||
94
app/Services/LotterySettings.php
Normal file
94
app/Services/LotterySettings.php
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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'),
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
};
|
||||
@@ -15,6 +15,7 @@ class DatabaseSeeder extends Seeder
|
||||
$this->call([
|
||||
CurrencySeeder::class,
|
||||
PlayTypeSeeder::class,
|
||||
LotterySettingsSeeder::class,
|
||||
]);
|
||||
|
||||
// 演示管理员 + 演示玩家:**勿在生产库执行**(或确保 APP_ENV≠production)
|
||||
|
||||
36
database/seeders/LotterySettingsSeeder.php
Normal file
36
database/seeders/LotterySettingsSeeder.php
Normal 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',
|
||||
'客户端展示用短名称(示例)',
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -8,47 +8,41 @@ use App\Http\Controllers\Api\V1\Wallet\WalletBalanceController;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
/*
|
||||
| Laravel 在 bootstrap 中为本文件自动加前缀 `api` + 中间件组 `api`,故实际 URL:
|
||||
| /api/v1/health
|
||||
| /api/v1/player/...
|
||||
| /api/v1/wallet/...
|
||||
| /api/v1/admin/...
|
||||
| Laravel 为本文件自动加前缀 `api`,此处再写 `v1`,故完整路径形如 `/api/v1/health`。
|
||||
*/
|
||||
|
||||
Route::prefix('v1')->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');
|
||||
});
|
||||
});
|
||||
|
||||
79
tests/Feature/AuditLoggerTest.php
Normal file
79
tests/Feature/AuditLoggerTest.php
Normal 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);
|
||||
});
|
||||
30
tests/Feature/LotterySettingsTest.php
Normal file
30
tests/Feature/LotterySettingsTest.php
Normal 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');
|
||||
});
|
||||
Reference in New Issue
Block a user