- 管理端账号/代理/站点等密码最短长度统一为 6 位 - LotterySettings、审计日志展示、币种与报表接口等小修复 - 补充设置批量更新、注单、校验错误等相关测试
263 lines
8.1 KiB
PHP
263 lines
8.1 KiB
PHP
<?php
|
||
|
||
namespace App\Services;
|
||
|
||
use App\Models\LotterySetting;
|
||
use Illuminate\Support\Facades\Cache;
|
||
|
||
/**
|
||
* 【配置中心】统一读入口:按 key 取运行期配置,默认带缓存。
|
||
*
|
||
* - 写入:控制台 / Seeder / 后续管理端可调 {@see put()};
|
||
* - 【重要】库里不存在的 key **不写入缓存**,避免长期命中默认值导致后台新增配置不生效。
|
||
*/
|
||
final class LotterySettings
|
||
{
|
||
/** 仅由 config/.env 控制,后台不可写入;旧 DB 行不再生效。 */
|
||
public const ENV_LOCKED_KEYS = [
|
||
'currency.default_code',
|
||
'draw.timezone',
|
||
'draw.interval_minutes',
|
||
'draw.betting_window_seconds',
|
||
'draw.close_before_draw_seconds',
|
||
'draw.buffer_draws_ahead',
|
||
];
|
||
|
||
private const CURRENCY_DISPLAY_DECIMALS = 2;
|
||
private const CURRENCY_DECIMAL_SEPARATOR = '.';
|
||
private const CURRENCY_THOUSANDS_SEPARATOR = ',';
|
||
|
||
public static function isEnvLocked(string $key): bool
|
||
{
|
||
return in_array($key, self::ENV_LOCKED_KEYS, true);
|
||
}
|
||
|
||
public static function defaultCurrency(): string
|
||
{
|
||
$value = (string) config('lottery.default_currency', 'NPR');
|
||
|
||
return strtoupper(substr(trim($value), 0, 16));
|
||
}
|
||
|
||
public static function drawTimezone(): string
|
||
{
|
||
return (string) config('lottery.draw.timezone', 'UTC');
|
||
}
|
||
|
||
public static function drawIntervalMinutes(): int
|
||
{
|
||
return max(1, min(1440, (int) config('lottery.draw.interval_minutes', 5)));
|
||
}
|
||
|
||
public static function drawBufferDrawsAhead(): int
|
||
{
|
||
return max(1, (int) config('lottery.draw.buffer_draws_ahead', 8));
|
||
}
|
||
|
||
public static function drawBettingWindowSeconds(): int
|
||
{
|
||
return max(10, (int) config('lottery.draw.betting_window_seconds', 270));
|
||
}
|
||
|
||
public static function drawCloseBeforeDrawSeconds(): int
|
||
{
|
||
return max(5, (int) config('lottery.draw.close_before_draw_seconds', 30));
|
||
}
|
||
|
||
/**
|
||
* 后台「部署参数」只读快照:供运营查看当前生效值与对应 env 变量名。
|
||
*
|
||
* @return array{
|
||
* items: list<array{key: string, label_key: string, value: mixed, env_var: string}>
|
||
* }
|
||
*/
|
||
public static function deploymentSnapshot(): array
|
||
{
|
||
return [
|
||
'items' => [
|
||
[
|
||
'key' => 'currency.default_code',
|
||
'label_key' => 'default_currency',
|
||
'value' => self::defaultCurrency(),
|
||
'env_var' => 'LOTTERY_DEFAULT_CURRENCY',
|
||
],
|
||
[
|
||
'key' => 'draw.timezone',
|
||
'label_key' => 'draw_timezone',
|
||
'value' => self::drawTimezone(),
|
||
'env_var' => 'LOTTERY_DRAW_TIMEZONE',
|
||
],
|
||
[
|
||
'key' => 'draw.interval_minutes',
|
||
'label_key' => 'draw_interval_minutes',
|
||
'value' => self::drawIntervalMinutes(),
|
||
'env_var' => 'LOTTERY_DRAW_INTERVAL_MINUTES',
|
||
],
|
||
[
|
||
'key' => 'draw.betting_window_seconds',
|
||
'label_key' => 'draw_betting_window_seconds',
|
||
'value' => self::drawBettingWindowSeconds(),
|
||
'env_var' => 'LOTTERY_DRAW_BETTING_WINDOW_SECONDS',
|
||
],
|
||
[
|
||
'key' => 'draw.close_before_draw_seconds',
|
||
'label_key' => 'draw_close_before_draw_seconds',
|
||
'value' => self::drawCloseBeforeDrawSeconds(),
|
||
'env_var' => 'LOTTERY_DRAW_CLOSE_BEFORE_SECONDS',
|
||
],
|
||
[
|
||
'key' => 'draw.buffer_draws_ahead',
|
||
'label_key' => 'draw_buffer_draws_ahead',
|
||
'value' => self::drawBufferDrawsAhead(),
|
||
'env_var' => 'LOTTERY_DRAW_BUFFER_AHEAD',
|
||
],
|
||
],
|
||
];
|
||
}
|
||
|
||
public static function drawRequireManualReview(): bool
|
||
{
|
||
$fallback = (bool) config('lottery.draw.require_manual_review', true);
|
||
|
||
return (bool) self::get('draw.require_manual_review', $fallback);
|
||
}
|
||
|
||
public static function drawCooldownMinutes(): int
|
||
{
|
||
$fallback = (int) config('lottery.draw.cooldown_minutes', 15);
|
||
|
||
return max(0, (int) self::get('draw.cooldown_minutes', $fallback));
|
||
}
|
||
|
||
public static function currencyDisplayDecimals(): int
|
||
{
|
||
return self::CURRENCY_DISPLAY_DECIMALS;
|
||
}
|
||
|
||
public static function currencyDecimalSeparator(): string
|
||
{
|
||
return self::CURRENCY_DECIMAL_SEPARATOR;
|
||
}
|
||
|
||
public static function currencyThousandsSeparator(): string
|
||
{
|
||
return self::CURRENCY_THOUSANDS_SEPARATOR;
|
||
}
|
||
|
||
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());
|
||
}
|
||
|
||
/**
|
||
* 批量写入(管理端一次保存多块配置,避免 N 次 HTTP + 审计)。
|
||
*
|
||
* @param list<array{key: string, value: mixed}> $items
|
||
*/
|
||
public static function putMany(array $items): void
|
||
{
|
||
if ($items === []) {
|
||
return;
|
||
}
|
||
|
||
$keys = array_values(array_unique(array_map(
|
||
static fn (array $item): string => (string) $item['key'],
|
||
$items,
|
||
)));
|
||
|
||
/** @var array<string, LotterySetting> $existing */
|
||
$existing = LotterySetting::query()
|
||
->whereIn('setting_key', $keys)
|
||
->get()
|
||
->keyBy('setting_key')
|
||
->all();
|
||
|
||
foreach ($items as $item) {
|
||
$key = (string) $item['key'];
|
||
$value = $item['value'];
|
||
$row = $existing[$key] ?? null;
|
||
|
||
self::put(
|
||
$key,
|
||
$value,
|
||
$row ? (string) $row->group_name : (explode('.', $key)[0] ?? 'general'),
|
||
$row ? $row->description_zh : null,
|
||
);
|
||
}
|
||
}
|
||
|
||
public static function cacheKey(string $key): string
|
||
{
|
||
return 'lottery_settings:'.$key;
|
||
}
|
||
|
||
private static function normalizeValue(mixed $value): mixed
|
||
{
|
||
return $value;
|
||
}
|
||
}
|