Files
lotteryLaravel/app/Services/LotterySettings.php
kang 3f04b4ebe3
Some checks failed
lotterLaravel CI / test (push) Has been cancelled
lotterLaravel E2E / e2e-api (push) Has been cancelled
feat(admin): 密码校验统一与后台 API/设置增强
- 管理端账号/代理/站点等密码最短长度统一为 6 位
- LotterySettings、审计日志展示、币种与报表接口等小修复
- 补充设置批量更新、注单、校验错误等相关测试
2026-06-26 14:39:22 +08:00

263 lines
8.1 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?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;
}
}