修改缓存方式

This commit is contained in:
2026-04-20 10:31:14 +08:00
parent 025cce3e3e
commit 92fb40ae80
19 changed files with 512 additions and 57 deletions

View File

@@ -19,8 +19,15 @@ DATABASE_HOSTPORT = 3306
DATABASE_CHARSET = utf8mb4 DATABASE_CHARSET = utf8mb4
DATABASE_PREFIX = DATABASE_PREFIX =
# 缓存config/cache.php # 框架缓存驱动config/cache.php → default供 Think-ORM 模型 Cache、get_sys_config 等使用;与 Redis 热点缓存无关
# 可选值见 cache.php 中 stores当前一般为 file
CACHE_DRIVER = file CACHE_DRIVER = file
# 游戏热点数据 Redisconfig/game_hot_cache.php用户 / game_config / game_record依赖 config/redis.php
GAME_HOT_CACHE_ENABLED = true
GAME_HOT_CACHE_TTL_GAME_CONFIG = 86400
GAME_HOT_CACHE_TTL_GAME_RECORD = 60
GAME_HOT_CACHE_TTL_USER = 90
# 移动端接口鉴权(/api/v1/authToken # 移动端接口鉴权(/api/v1/authToken
AUTH_TOKEN_SECRET = 564d14asdasd113e46542asd6das1a2a AUTH_TOKEN_SECRET = 564d14asdasd113e46542asd6das1a2a

View File

@@ -4,6 +4,7 @@ namespace app\admin\controller\config;
use app\common\controller\Backend; use app\common\controller\Backend;
use app\common\library\game\DepositTier as DepositTierLib; use app\common\library\game\DepositTier as DepositTierLib;
use app\common\service\GameHotDataRedis;
use InvalidArgumentException; use InvalidArgumentException;
use support\think\Db; use support\think\Db;
use support\Response; use support\Response;
@@ -125,6 +126,8 @@ class DepositTier extends Backend
return $this->error($e->getMessage()); return $this->error($e->getMessage());
} }
GameHotDataRedis::gameConfigForget(DepositTierLib::CONFIG_KEY);
return $this->success(__('Saved successfully')); return $this->success(__('Saved successfully'));
} }
} }

View File

@@ -6,6 +6,7 @@ namespace app\admin\controller\config;
use app\common\controller\Backend; use app\common\controller\Backend;
use app\common\library\game\StreakWinReward as StreakWinRewardLib; use app\common\library\game\StreakWinReward as StreakWinRewardLib;
use app\common\service\GameHotDataRedis;
use support\think\Db; use support\think\Db;
use support\Response; use support\Response;
use Throwable; use Throwable;
@@ -115,6 +116,7 @@ class StreakWinReward extends Backend
return $this->error($e->getMessage()); return $this->error($e->getMessage());
} }
StreakWinRewardLib::clearCache(); StreakWinRewardLib::clearCache();
GameHotDataRedis::gameConfigForget(StreakWinRewardLib::CONFIG_KEY);
return $this->success('保存成功'); return $this->success('保存成功');
} }

View File

@@ -3,6 +3,7 @@
namespace app\admin\controller\config; namespace app\admin\controller\config;
use app\common\controller\Backend; use app\common\controller\Backend;
use app\common\service\GameHotDataRedis;
use app\common\library\game\ZiHuaDictionary as ZiHuaDictionaryLib; use app\common\library\game\ZiHuaDictionary as ZiHuaDictionaryLib;
use InvalidArgumentException; use InvalidArgumentException;
use support\think\Db; use support\think\Db;
@@ -125,6 +126,8 @@ class ZiHuaDictionary extends Backend
return $this->error($e->getMessage()); return $this->error($e->getMessage());
} }
GameHotDataRedis::gameConfigForget(ZiHuaDictionaryLib::CONFIG_KEY);
return $this->success(__('Saved successfully')); return $this->success(__('Saved successfully'));
} }
} }

View File

@@ -71,9 +71,11 @@ class Live extends Backend
$manualNumber = is_numeric((string) $manualRaw) ? (int) $manualRaw : null; $manualNumber = is_numeric((string) $manualRaw) ? (int) $manualRaw : null;
$res = GameLiveService::calculateResult($recordId, $manualNumber); $res = GameLiveService::calculateResult($recordId, $manualNumber);
if (!($res['ok'] ?? false)) { if (!($res['ok'] ?? false)) {
return $this->error((string) ($res['msg'] ?? '计算失败')); $errMsg = $res['msg'] ?? null;
return $this->error(is_string($errMsg) ? $errMsg : __('Calculation failed'));
} }
return $this->success((string) $res['msg'], $res); $okMsg = $res['msg'] ?? '';
return $this->success(is_string($okMsg) ? $okMsg : '', $res);
} }
/** /**
@@ -92,13 +94,15 @@ class Live extends Backend
$recordId = is_numeric((string) $recordIdRaw) ? (int) $recordIdRaw : null; $recordId = is_numeric((string) $recordIdRaw) ? (int) $recordIdRaw : null;
$manualRaw = $request->post('manual_number'); $manualRaw = $request->post('manual_number');
if (!is_numeric((string) $manualRaw)) { if (!is_numeric((string) $manualRaw)) {
return $this->error('请填写开奖号码'); return $this->error(__('Please enter the draw number'));
} }
$manualNumber = (int) $manualRaw; $manualNumber = (int) $manualRaw;
$res = GameLiveService::scheduleDraw($recordId, $manualNumber); $res = GameLiveService::scheduleDraw($recordId, $manualNumber);
if (!($res['ok'] ?? false)) { if (!($res['ok'] ?? false)) {
return $this->error((string) ($res['msg'] ?? '预约失败')); $errMsg = $res['msg'] ?? null;
return $this->error(is_string($errMsg) ? $errMsg : __('Schedule failed'));
} }
return $this->success((string) $res['msg'], $res); $okMsg = $res['msg'] ?? '';
return $this->success(is_string($okMsg) ? $okMsg : '', $res);
} }
} }

View File

@@ -4,6 +4,7 @@ namespace app\admin\controller\game;
use app\common\controller\Backend; use app\common\controller\Backend;
use app\common\library\game\ZiHuaDictionary as ZiHuaDictionaryLib; use app\common\library\game\ZiHuaDictionary as ZiHuaDictionaryLib;
use app\common\service\GameHotDataRedis;
use InvalidArgumentException; use InvalidArgumentException;
use support\think\Db; use support\think\Db;
use support\Response; use support\Response;
@@ -111,6 +112,8 @@ class ZiHuaDictionary extends Backend
return $this->error($e->getMessage()); return $this->error($e->getMessage());
} }
GameHotDataRedis::gameConfigForget(ZiHuaDictionaryLib::CONFIG_KEY);
return $this->success(__('Saved successfully')); return $this->success(__('Saved successfully'));
} }
} }

View File

@@ -277,7 +277,30 @@ class Auth extends \ba\Auth
public function getMenus(int $uid = 0): array public function getMenus(int $uid = 0): array
{ {
return parent::getMenus($uid ?: $this->id); $menus = parent::getMenus($uid ?: $this->id);
return $this->translateMenuRuleTitles($menus);
}
/**
* 将 admin_rule.title库内中文等按当前请求语言翻译供侧边栏/标签与 meta.title 一致。
* 英文词条见 app/common/lang/en/admin_rule_title.php
*
* @param array<int, array<string, mixed>> $menus
* @return array<int, array<string, mixed>>
*/
private function translateMenuRuleTitles(array $menus): array
{
foreach ($menus as $k => $item) {
if (isset($item['title']) && is_string($item['title']) && $item['title'] !== '') {
$menus[$k]['title'] = __($item['title']);
}
if (!empty($item['children']) && is_array($item['children'])) {
$menus[$k]['children'] = $this->translateMenuRuleTitles($item['children']);
}
}
return $menus;
} }
public function isSuperAdmin(): bool public function isSuperAdmin(): bool

View File

@@ -6,9 +6,9 @@ namespace app\api\controller;
use app\common\library\game\ZiHuaDictionary; use app\common\library\game\ZiHuaDictionary;
use app\common\model\BetOrder; use app\common\model\BetOrder;
use app\common\model\GameConfig;
use app\common\model\GameRecord; use app\common\model\GameRecord;
use app\common\model\UserWalletRecord; use app\common\model\UserWalletRecord;
use app\common\service\GameHotDataRedis;
use app\common\service\UserPushService; use app\common\service\UserPushService;
use support\think\Db; use support\think\Db;
use Webman\Http\Request; use Webman\Http\Request;
@@ -25,15 +25,15 @@ class Game extends MobileBase
return $response; return $response;
} }
$period = GameRecord::order('id', 'desc')->find(); $periodRow = GameHotDataRedis::gameRecordLatest();
$now = time(); $now = time();
$startAt = $period ? $this->intValue($period->period_start_at) : $now; $startAt = $periodRow ? $this->intValue($periodRow['period_start_at'] ?? 0) : $now;
$lockAt = $startAt + 20; $lockAt = $startAt + 20;
$openAt = $startAt + 22; $openAt = $startAt + 22;
$countdown = $period ? max(0, ($startAt + 30) - $now) : 0; $countdown = $periodRow ? max(0, ($startAt + 30) - $now) : 0;
$dictionaryConfig = GameConfig::where('config_key', ZiHuaDictionary::CONFIG_KEY)->find(); $dictionaryConfig = GameHotDataRedis::gameConfigRow(ZiHuaDictionary::CONFIG_KEY) ?? [];
$dictionaryItems = ZiHuaDictionary::parseFromConfigValue($dictionaryConfig?->config_value ?? null); $dictionaryItems = ZiHuaDictionary::parseFromConfigValue($dictionaryConfig['config_value'] ?? null);
$items = []; $items = [];
foreach ($dictionaryItems as $row) { foreach ($dictionaryItems as $row) {
$items[] = [ $items[] = [
@@ -48,8 +48,8 @@ class Game extends MobileBase
return $this->mobileSuccess([ return $this->mobileSuccess([
'server_time' => $now, 'server_time' => $now,
'period' => [ 'period' => [
'period_no' => $period->period_no ?? '', 'period_no' => (string) ($periodRow['period_no'] ?? ''),
'status' => $this->mapPeriodStatus($period->status ?? null), 'status' => $this->mapPeriodStatus($periodRow['status'] ?? null),
'countdown' => $countdown, 'countdown' => $countdown,
'lock_at' => $lockAt, 'lock_at' => $lockAt,
'open_at' => $openAt, 'open_at' => $openAt,
@@ -73,8 +73,8 @@ class Game extends MobileBase
if ($response !== null) { if ($response !== null) {
return $response; return $response;
} }
$dictionaryConfig = GameConfig::where('config_key', ZiHuaDictionary::CONFIG_KEY)->find(); $dictionaryConfig = GameHotDataRedis::gameConfigRow(ZiHuaDictionary::CONFIG_KEY) ?? [];
$dictionaryItems = ZiHuaDictionary::parseFromConfigValue($dictionaryConfig?->config_value ?? null); $dictionaryItems = ZiHuaDictionary::parseFromConfigValue($dictionaryConfig['config_value'] ?? null);
$items = []; $items = [];
foreach ($dictionaryItems as $row) { foreach ($dictionaryItems as $row) {
$items[] = [ $items[] = [
@@ -85,7 +85,7 @@ class Game extends MobileBase
]; ];
} }
return $this->mobileSuccess([ return $this->mobileSuccess([
'version' => (string) ($dictionaryConfig->update_time ?? '1'), 'version' => (string) ($dictionaryConfig['update_time'] ?? '1'),
'items' => $items, 'items' => $items,
]); ]);
} }
@@ -118,19 +118,19 @@ class Game extends MobileBase
if ($response !== null) { if ($response !== null) {
return $response; return $response;
} }
$period = GameRecord::order('id', 'desc')->find(); $periodRow = GameHotDataRedis::gameRecordLatest();
if (!$period) { if (!$periodRow) {
return $this->mobileError(2002, 'Game period does not exist'); return $this->mobileError(2002, 'Game period does not exist');
} }
$now = time(); $now = time();
$startAt = $this->intValue($period->period_start_at); $startAt = $this->intValue($periodRow['period_start_at'] ?? 0);
return $this->mobileSuccess([ return $this->mobileSuccess([
'period_id' => $period->id, 'period_id' => $periodRow['id'],
'period_no' => $period->period_no, 'period_no' => $periodRow['period_no'],
'status' => $this->mapPeriodStatus($period->status), 'status' => $this->mapPeriodStatus($periodRow['status'] ?? null),
'countdown' => max(0, ($startAt + 30) - $now), 'countdown' => max(0, ($startAt + 30) - $now),
'bet_close_in' => max(0, ($startAt + 20) - $now), 'bet_close_in' => max(0, ($startAt + 20) - $now),
'result_number' => $period->result_number, 'result_number' => $periodRow['result_number'] ?? null,
]); ]);
} }
@@ -218,6 +218,10 @@ class Game extends MobileBase
'update_time' => time(), 'update_time' => time(),
]); ]);
Db::commit(); Db::commit();
$uid = filter_var($user->id, FILTER_VALIDATE_INT);
if ($uid !== false) {
GameHotDataRedis::userForget($uid);
}
UserPushService::publish((int) $user->id, UserPushService::EVT_BET_ACCEPTED, [ UserPushService::publish((int) $user->id, UserPushService::EVT_BET_ACCEPTED, [
'order_no' => $orderNo, 'order_no' => $orderNo,
'period_no' => (string) $period->period_no, 'period_no' => (string) $period->period_no,
@@ -355,7 +359,11 @@ class Game extends MobileBase
private function getConfigValue(string $key, string $default): string private function getConfigValue(string $key, string $default): string
{ {
$value = GameConfig::where('config_key', $key)->value('config_value'); $row = GameHotDataRedis::gameConfigRow($key);
if ($row === null) {
return $default;
}
$value = $row['config_value'] ?? null;
if ($value === null || $value === '') { if ($value === null || $value === '') {
return $default; return $default;
} }

View File

@@ -7,6 +7,7 @@ use ba\Random;
use support\think\Db; use support\think\Db;
use app\common\model\User; use app\common\model\User;
use app\common\facade\Token; use app\common\facade\Token;
use app\common\service\GameHotDataRedis;
use Webman\Http\Request; use Webman\Http\Request;
/** /**
@@ -69,7 +70,7 @@ class Auth extends \ba\Auth
Token::tokenExpirationCheck($tokenData); Token::tokenExpirationCheck($tokenData);
$userId = $tokenData['user_id']; $userId = $tokenData['user_id'];
if ($tokenData['type'] == self::TOKEN_TYPE && $userId > 0) { if ($tokenData['type'] == self::TOKEN_TYPE && $userId > 0) {
$this->model = User::where('id', $userId)->find(); $this->model = GameHotDataRedis::userModelFromCacheOrDb($userId);
if (!$this->model) { if (!$this->model) {
$this->setError('Account not exist'); $this->setError('Account not exist');
return false; return false;

View File

@@ -4,7 +4,7 @@ declare(strict_types=1);
namespace app\common\library\finance; namespace app\common\library\finance;
use support\think\Db; use app\common\service\GameHotDataRedis;
/** /**
* 提现打码量(流水)门槛工具库 * 提现打码量(流水)门槛工具库
@@ -38,7 +38,7 @@ final class WithdrawFlow
*/ */
public static function ratio(): string public static function ratio(): string
{ {
$row = Db::name('game_config')->where('config_key', self::CONFIG_KEY)->find(); $row = GameHotDataRedis::gameConfigRow(self::CONFIG_KEY);
if (!$row) { if (!$row) {
return self::DEFAULT_RATIO; return self::DEFAULT_RATIO;
} }

View File

@@ -4,7 +4,7 @@ declare(strict_types=1);
namespace app\common\library\game; namespace app\common\library\game;
use support\think\Db; use app\common\service\GameHotDataRedis;
/** /**
* 连胜奖励game_config.streak_win_reward按「连胜档位」110 配置赔率系数与是否大奖。 * 连胜奖励game_config.streak_win_reward按「连胜档位」110 配置赔率系数与是否大奖。
@@ -100,7 +100,7 @@ final class StreakWinReward
if (self::$cache !== null) { if (self::$cache !== null) {
return self::$cache; return self::$cache;
} }
$row = Db::name('game_config')->where('config_key', self::CONFIG_KEY)->find(); $row = GameHotDataRedis::gameConfigRow(self::CONFIG_KEY);
self::$cache = self::parseFromConfigValue($row['config_value'] ?? null); self::$cache = self::parseFromConfigValue($row['config_value'] ?? null);
return self::$cache; return self::$cache;

View File

@@ -23,33 +23,87 @@ class LoadLangPack implements MiddlewareInterface
return $handler($request); return $handler($request);
} }
/**
* 解析当前请求语言。
* - 后台 admin优先请求头 think-langzh-cn / en其次 lang 头,再次查询/表单参数 lang支持 zh→zh-cn
* - 对外 api优先查询/表单参数 langzh / en其次 lang 头,再次 think-lang。
*/
protected function resolveLangSet(Request $request): string
{
$path = trim($request->path(), '/');
$parts = explode('/', $path);
$app = $parts[0] ?? 'api';
$queryRaw = $request->get('lang');
if ($queryRaw === null || $queryRaw === '') {
$queryRaw = $request->post('lang');
}
$queryLang = is_string($queryRaw) ? $queryRaw : '';
$thinkRaw = $request->header('think-lang');
$thinkLang = is_string($thinkRaw) ? $thinkRaw : '';
$headerLangRaw = $request->header('lang');
$headerLang = is_string($headerLangRaw) ? $headerLangRaw : '';
$normalize = static function (string $raw): string {
$s = str_replace('_', '-', strtolower(trim($raw)));
if ($s === 'zh') {
return 'zh-cn';
}
return $s;
};
$allowLangList = config('lang.allow_lang_list', ['zh-cn', 'en']);
$candidates = [];
if ($app === 'admin') {
if ($thinkLang !== '') {
$candidates[] = $normalize($thinkLang);
}
if ($headerLang !== '') {
$candidates[] = $normalize($headerLang);
}
if ($queryLang !== '') {
$candidates[] = $normalize($queryLang);
}
} else {
if ($queryLang !== '') {
$candidates[] = $normalize($queryLang);
}
if ($headerLang !== '') {
$candidates[] = $normalize($headerLang);
}
if ($thinkLang !== '') {
$candidates[] = $normalize($thinkLang);
}
}
foreach ($candidates as $code) {
if ($code !== '' && in_array($code, $allowLangList, true)) {
return $code;
}
}
$acceptRaw = $request->header('accept-language');
$acceptLang = is_string($acceptRaw) ? $acceptRaw : '';
if (preg_match('/^zh[-_]?cn|^zh/i', $acceptLang)) {
return 'zh-cn';
}
if (preg_match('/^en/i', $acceptLang)) {
return 'en';
}
$defaultRaw = config('lang.default_lang', config('translation.locale', 'zh-cn'));
$default = is_string($defaultRaw) ? $defaultRaw : 'zh-cn';
return str_replace('_', '-', strtolower($default));
}
protected function loadLang(Request $request): void protected function loadLang(Request $request): void
{ {
// 优先从请求头 lang / think-lang 获取前端选择的语言 $langSet = $this->resolveLangSet($request);
// 支持lang=en / lang=zh / think-lang=en / think-lang=zh-cn
// 未发送时回退到 Accept-Language 或配置默认值
$headerLang = $request->header('lang', '');
if ($headerLang === '') {
$headerLang = $request->header('think-lang', '');
}
$allowLangList = config('lang.allow_lang_list', ['zh-cn', 'en']);
$normalizedHeaderLang = str_replace('_', '-', strtolower($headerLang));
if ($normalizedHeaderLang === 'zh') {
$normalizedHeaderLang = 'zh-cn';
}
if ($headerLang && in_array($normalizedHeaderLang, $allowLangList)) {
$langSet = $normalizedHeaderLang;
} else {
$acceptLang = $request->header('accept-language', '');
if (preg_match('/^zh[-_]?cn|^zh/i', $acceptLang)) {
$langSet = 'zh-cn';
} elseif (preg_match('/^en/i', $acceptLang)) {
$langSet = 'en';
} else {
$langSet = config('lang.default_lang', config('translation.locale', 'zh-cn'));
}
$langSet = str_replace('_', '-', strtolower($langSet));
}
// 设置当前请求的翻译语言,使 __() 和 trans() 使用正确的语言 // 设置当前请求的翻译语言,使 __() 和 trans() 使用正确的语言
if (function_exists('locale')) { if (function_exists('locale')) {
@@ -74,6 +128,12 @@ class LoadLangPack implements MiddlewareInterface
$translator->addResource('phpfile', $rootLangFile, $langSet, 'messages'); $translator->addResource('phpfile', $rootLangFile, $langSet, 'messages');
} }
// 1.5 公共语言包app/common/lang/{locale}/*.php供 common 服务层与多应用共用
$commonLangPattern = base_path() . DIRECTORY_SEPARATOR . 'app' . DIRECTORY_SEPARATOR . 'common' . DIRECTORY_SEPARATOR . 'lang' . DIRECTORY_SEPARATOR . $langSet . DIRECTORY_SEPARATOR . '*.php';
foreach (glob($commonLangPattern) ?: [] as $commonLangFile) {
$translator->addResource('phpfile', $commonLangFile, $langSet, 'messages');
}
// 2. 加载控制器专用语言包(如 zh-cn/auth/group.php供 get_route_remark 等使用 // 2. 加载控制器专用语言包(如 zh-cn/auth/group.php供 get_route_remark 等使用
// 同时加载到 messages 域,使 __() 能正确翻译控制器内的文案(如安装页错误提示) // 同时加载到 messages 域,使 __() 能正确翻译控制器内的文案(如安装页错误提示)
$controllerPath = get_controller_path($request); $controllerPath = get_controller_path($request);

View File

@@ -2,6 +2,8 @@
namespace app\common\model; namespace app\common\model;
use app\common\library\game\StreakWinReward;
use app\common\service\GameHotDataRedis;
use support\think\Model; use support\think\Model;
/** /**
@@ -17,4 +19,26 @@ class GameConfig extends Model
'create_time' => 'integer', 'create_time' => 'integer',
'update_time' => 'integer', 'update_time' => 'integer',
]; ];
public static function onAfterWrite(GameConfig $model): void
{
$key = trim((string) ($model->getAttr('config_key') ?? ''));
if ($key !== '') {
GameHotDataRedis::gameConfigForget($key);
}
if ($key === StreakWinReward::CONFIG_KEY) {
StreakWinReward::clearCache();
}
}
public static function onAfterDelete(GameConfig $model): void
{
$key = trim((string) ($model->getAttr('config_key') ?? ''));
if ($key !== '') {
GameHotDataRedis::gameConfigForget($key);
}
if ($key === StreakWinReward::CONFIG_KEY) {
StreakWinReward::clearCache();
}
}
} }

View File

@@ -2,6 +2,7 @@
namespace app\common\model; namespace app\common\model;
use app\common\service\GameHotDataRedis;
use support\think\Model; use support\think\Model;
class GameRecord extends Model class GameRecord extends Model
@@ -41,4 +42,16 @@ class GameRecord extends Model
} }
return 0; return 0;
} }
public static function onAfterWrite(GameRecord $model): void
{
$id = filter_var($model->getAttr('id'), FILTER_VALIDATE_INT);
GameHotDataRedis::gameRecordForget($id === false ? null : $id);
}
public static function onAfterDelete(GameRecord $model): void
{
$id = filter_var($model->getAttr('id'), FILTER_VALIDATE_INT);
GameHotDataRedis::gameRecordForget($id === false ? null : $id);
}
} }

View File

@@ -2,6 +2,7 @@
namespace app\common\model; namespace app\common\model;
use app\common\service\GameHotDataRedis;
use support\think\Model; use support\think\Model;
/** /**
@@ -62,4 +63,20 @@ class User extends Model
{ {
return $this->where(['id' => $uid])->update(['password' => hash_password($newPassword)]); return $this->where(['id' => $uid])->update(['password' => hash_password($newPassword)]);
} }
public static function onAfterWrite(User $model): void
{
$id = filter_var($model->getAttr('id'), FILTER_VALIDATE_INT);
if ($id !== false && $id > 0) {
GameHotDataRedis::userForget($id);
}
}
public static function onAfterDelete(User $model): void
{
$id = filter_var($model->getAttr('id'), FILTER_VALIDATE_INT);
if ($id !== false && $id > 0) {
GameHotDataRedis::userForget($id);
}
}
} }

View File

@@ -131,6 +131,7 @@ final class GameBetSettleService
'current_streak' => $next, 'current_streak' => $next,
'update_time' => $now, 'update_time' => $now,
]); ]);
GameHotDataRedis::userForget($userId);
} }
foreach ($aggregateByUser as $userId => $agg) { foreach ($aggregateByUser as $userId => $agg) {
@@ -272,6 +273,7 @@ final class GameBetSettleService
'bet_flow_coin' => Db::raw('bet_flow_coin + ' . $flow), 'bet_flow_coin' => Db::raw('bet_flow_coin + ' . $flow),
'update_time' => $now, 'update_time' => $now,
]); ]);
GameHotDataRedis::userForget($userId);
} }
/** /**
@@ -319,6 +321,7 @@ final class GameBetSettleService
'coin' => $after, 'coin' => $after,
'update_time' => $now, 'update_time' => $now,
]); ]);
GameHotDataRedis::userForget($userId);
return $after; return $after;
} }

View File

@@ -0,0 +1,279 @@
<?php
declare(strict_types=1);
namespace app\common\service;
use app\common\model\User;
use support\Redis;
use support\think\Db;
use Throwable;
/**
* 热点数据 Redis 缓存game_config、game_record、user 全行 JSON。
* Redis 不可用时自动回退为直连数据库。
*/
final class GameHotDataRedis
{
private const KEY_GC = 'dfw:v1:gc:';
private const KEY_GR_ID = 'dfw:v1:gr:id:';
private const KEY_GR_ACTIVE = 'dfw:v1:gr:active';
private const KEY_GR_LATEST = 'dfw:v1:gr:latest';
private const KEY_USER = 'dfw:v1:user:';
public static function enabled(): bool
{
return config('game_hot_cache.enabled', true) === true;
}
/**
* @return array<string, mixed>|null
*/
public static function gameConfigRow(string $configKey): ?array
{
if ($configKey === '') {
return null;
}
if (self::enabled()) {
$cached = self::redisGet(self::KEY_GC . $configKey);
if ($cached !== null && $cached !== '') {
$decoded = json_decode($cached, true);
if (is_array($decoded)) {
return $decoded;
}
}
}
$row = Db::name('game_config')->where('config_key', $configKey)->find();
if (!$row) {
return null;
}
if (self::enabled()) {
$ttl = self::intConfig('ttl_game_config', 86400);
self::redisSetEx(self::KEY_GC . $configKey, $ttl, json_encode($row, JSON_UNESCAPED_UNICODE));
}
return $row;
}
public static function gameConfigForget(string $configKey): void
{
if ($configKey === '') {
return;
}
self::redisDel(self::KEY_GC . $configKey);
}
/**
* @return array<string, mixed>|null
*/
public static function gameRecordById(int $id): ?array
{
if ($id <= 0) {
return null;
}
$key = self::KEY_GR_ID . $id;
if (self::enabled()) {
$cached = self::redisGet($key);
if ($cached !== null && $cached !== '') {
$decoded = json_decode($cached, true);
if (is_array($decoded)) {
return $decoded;
}
}
}
$row = Db::name('game_record')->where('id', $id)->find();
if (!$row) {
return null;
}
if (self::enabled()) {
$ttl = self::intConfig('ttl_game_record', 60);
self::redisSetEx($key, $ttl, json_encode($row, JSON_UNESCAPED_UNICODE));
}
return $row;
}
/**
* 当前未完全结束的对局status ∈ 0,1,2,3 中 id 最大的一条)
*
* @return array<string, mixed>|null
*/
public static function gameRecordActive(): ?array
{
if (self::enabled()) {
$cached = self::redisGet(self::KEY_GR_ACTIVE);
if ($cached !== null && $cached !== '') {
$decoded = json_decode($cached, true);
if (is_array($decoded)) {
return $decoded;
}
}
}
$row = Db::name('game_record')->whereIn('status', [0, 1, 2, 3])->order('id', 'desc')->find();
if (!$row) {
return null;
}
if (self::enabled()) {
$ttl = self::intConfig('ttl_game_record', 60);
self::redisSetEx(self::KEY_GR_ACTIVE, $ttl, json_encode($row, JSON_UNESCAPED_UNICODE));
}
return $row;
}
/**
* 最新一条对局(含已结束),与 GameRecord::order('id','desc')->find() 一致
*
* @return array<string, mixed>|null
*/
public static function gameRecordLatest(): ?array
{
if (self::enabled()) {
$cached = self::redisGet(self::KEY_GR_LATEST);
if ($cached !== null && $cached !== '') {
$decoded = json_decode($cached, true);
if (is_array($decoded)) {
return $decoded;
}
}
}
$row = Db::name('game_record')->order('id', 'desc')->find();
if (!$row) {
return null;
}
if (self::enabled()) {
$ttl = self::intConfig('ttl_game_record', 60);
self::redisSetEx(self::KEY_GR_LATEST, $ttl, json_encode($row, JSON_UNESCAPED_UNICODE));
}
return $row;
}
/**
* 任意 game_record 变更后调用:删除活跃局、最新局及指定 id 缓存
*/
public static function gameRecordForget(?int $id = null): void
{
$keys = [self::KEY_GR_ACTIVE, self::KEY_GR_LATEST];
if ($id !== null && $id > 0) {
$keys[] = self::KEY_GR_ID . $id;
}
self::redisDel(...$keys);
}
/**
* @return array<string, mixed>|null
*/
public static function userRow(int $userId): ?array
{
if ($userId <= 0) {
return null;
}
$key = self::KEY_USER . $userId;
if (self::enabled()) {
$cached = self::redisGet($key);
if ($cached !== null && $cached !== '') {
$decoded = json_decode($cached, true);
if (is_array($decoded)) {
return $decoded;
}
}
}
$row = Db::name('user')->where('id', $userId)->find();
if (!$row) {
return null;
}
if (self::enabled()) {
$ttl = self::intConfig('ttl_user', 90);
self::redisSetEx($key, $ttl, json_encode($row, JSON_UNESCAPED_UNICODE));
}
return $row;
}
public static function userRemember(User $user): void
{
if (!self::enabled()) {
return;
}
$id = $user->getAttr('id');
$uid = filter_var($id, FILTER_VALIDATE_INT);
if ($uid === false || $uid <= 0) {
return;
}
$ttl = self::intConfig('ttl_user', 90);
self::redisSetEx(self::KEY_USER . $uid, $ttl, json_encode($user->toArray(), JSON_UNESCAPED_UNICODE));
}
public static function userForget(int $userId): void
{
if ($userId <= 0) {
return;
}
self::redisDel(self::KEY_USER . $userId);
}
/**
* 用缓存行构造已存在库的 User供 Auth 等高频读)
*/
public static function userModelFromCacheOrDb(int $userId): ?User
{
$row = self::userRow($userId);
if ($row === null) {
return null;
}
return (new User())->newInstance($row);
}
private static function intConfig(string $name, int $default): int
{
$v = config('game_hot_cache.' . $name, $default);
if (is_int($v)) {
return $v > 0 ? $v : $default;
}
if (is_numeric($v)) {
$n = filter_var($v, FILTER_VALIDATE_INT);
if ($n === false) {
return $default;
}
return $n > 0 ? $n : $default;
}
return $default;
}
private static function redisGet(string $key): ?string
{
try {
$v = Redis::get($key);
if ($v === false || $v === null) {
return null;
}
if (!is_string($v)) {
return null;
}
return $v;
} catch (Throwable) {
return null;
}
}
private static function redisSetEx(string $key, int $ttl, string $value): void
{
try {
Redis::setEx($key, $ttl, $value);
} catch (Throwable) {
}
}
private static function redisDel(string ...$keys): void
{
if ($keys === []) {
return;
}
try {
Redis::del(...$keys);
} catch (Throwable) {
}
}
}

View File

@@ -17,7 +17,7 @@ final class GameRecordService
public static function getConfigBool(string $key): bool public static function getConfigBool(string $key): bool
{ {
$row = Db::name('game_config')->where('config_key', $key)->find(); $row = GameHotDataRedis::gameConfigRow($key);
if (!$row) { if (!$row) {
return false; return false;
} }
@@ -99,6 +99,7 @@ final class GameRecordService
'create_time' => $now, 'create_time' => $now,
'update_time' => $now, 'update_time' => $now,
]); ]);
GameHotDataRedis::gameRecordForget();
return $periodNo; return $periodNo;
} }
@@ -122,6 +123,7 @@ final class GameRecordService
'remark' => $remark, 'remark' => $remark,
'update_time' => $now, 'update_time' => $now,
]); ]);
GameHotDataRedis::gameConfigForget($key);
return; return;
} }
Db::name('game_config')->insert([ Db::name('game_config')->insert([
@@ -132,5 +134,6 @@ final class GameRecordService
'create_time' => $now, 'create_time' => $now,
'update_time' => $now, 'update_time' => $now,
]); ]);
GameHotDataRedis::gameConfigForget($key);
} }
} }

View File

@@ -33,6 +33,7 @@ final class GameRecordStatService
'winner_user_count' => 0, 'winner_user_count' => 0,
'update_time' => $now, 'update_time' => $now,
]); ]);
GameHotDataRedis::gameRecordForget($recordId);
return; return;
} }
@@ -78,6 +79,7 @@ final class GameRecordStatService
'winner_user_count' => count($winnerUserIds), 'winner_user_count' => count($winnerUserIds),
'update_time' => $now, 'update_time' => $now,
]); ]);
GameHotDataRedis::gameRecordForget($recordId);
} }
/** /**