From 92fb40ae80c2dcf95f544ea84c3b45982619c324 Mon Sep 17 00:00:00 2001 From: zhenhui <1276357500@qq.com> Date: Mon, 20 Apr 2026 10:31:14 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E7=BC=93=E5=AD=98=E6=96=B9?= =?UTF-8?q?=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env-example | 9 +- app/admin/controller/config/DepositTier.php | 3 + .../controller/config/StreakWinReward.php | 2 + .../controller/config/ZiHuaDictionary.php | 3 + app/admin/controller/game/Live.php | 14 +- app/admin/controller/game/ZiHuaDictionary.php | 3 + app/admin/library/Auth.php | 25 +- app/api/controller/Game.php | 46 +-- app/common/library/Auth.php | 3 +- app/common/library/finance/WithdrawFlow.php | 4 +- app/common/library/game/StreakWinReward.php | 4 +- app/common/middleware/LoadLangPack.php | 110 +++++-- app/common/model/GameConfig.php | 24 ++ app/common/model/GameRecord.php | 13 + app/common/model/User.php | 17 ++ app/common/service/GameBetSettleService.php | 3 + app/common/service/GameHotDataRedis.php | 279 ++++++++++++++++++ app/common/service/GameRecordService.php | 5 +- app/common/service/GameRecordStatService.php | 2 + 19 files changed, 512 insertions(+), 57 deletions(-) create mode 100644 app/common/service/GameHotDataRedis.php diff --git a/.env-example b/.env-example index 245c9e1..640566e 100644 --- a/.env-example +++ b/.env-example @@ -19,8 +19,15 @@ DATABASE_HOSTPORT = 3306 DATABASE_CHARSET = utf8mb4 DATABASE_PREFIX = -# 缓存(config/cache.php) +# 框架缓存驱动(config/cache.php → default,供 Think-ORM 模型 Cache、get_sys_config 等使用;与 Redis 热点缓存无关) +# 可选值见 cache.php 中 stores,当前一般为 file CACHE_DRIVER = file +# 游戏热点数据 Redis(config/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) AUTH_TOKEN_SECRET = 564d14asdasd113e46542asd6das1a2a diff --git a/app/admin/controller/config/DepositTier.php b/app/admin/controller/config/DepositTier.php index e612f02..6ab2725 100644 --- a/app/admin/controller/config/DepositTier.php +++ b/app/admin/controller/config/DepositTier.php @@ -4,6 +4,7 @@ namespace app\admin\controller\config; use app\common\controller\Backend; use app\common\library\game\DepositTier as DepositTierLib; +use app\common\service\GameHotDataRedis; use InvalidArgumentException; use support\think\Db; use support\Response; @@ -125,6 +126,8 @@ class DepositTier extends Backend return $this->error($e->getMessage()); } + GameHotDataRedis::gameConfigForget(DepositTierLib::CONFIG_KEY); + return $this->success(__('Saved successfully')); } } diff --git a/app/admin/controller/config/StreakWinReward.php b/app/admin/controller/config/StreakWinReward.php index 77d4eec..c9a92aa 100644 --- a/app/admin/controller/config/StreakWinReward.php +++ b/app/admin/controller/config/StreakWinReward.php @@ -6,6 +6,7 @@ namespace app\admin\controller\config; use app\common\controller\Backend; use app\common\library\game\StreakWinReward as StreakWinRewardLib; +use app\common\service\GameHotDataRedis; use support\think\Db; use support\Response; use Throwable; @@ -115,6 +116,7 @@ class StreakWinReward extends Backend return $this->error($e->getMessage()); } StreakWinRewardLib::clearCache(); + GameHotDataRedis::gameConfigForget(StreakWinRewardLib::CONFIG_KEY); return $this->success('保存成功'); } diff --git a/app/admin/controller/config/ZiHuaDictionary.php b/app/admin/controller/config/ZiHuaDictionary.php index 4c9404b..46064f0 100644 --- a/app/admin/controller/config/ZiHuaDictionary.php +++ b/app/admin/controller/config/ZiHuaDictionary.php @@ -3,6 +3,7 @@ namespace app\admin\controller\config; use app\common\controller\Backend; +use app\common\service\GameHotDataRedis; use app\common\library\game\ZiHuaDictionary as ZiHuaDictionaryLib; use InvalidArgumentException; use support\think\Db; @@ -125,6 +126,8 @@ class ZiHuaDictionary extends Backend return $this->error($e->getMessage()); } + GameHotDataRedis::gameConfigForget(ZiHuaDictionaryLib::CONFIG_KEY); + return $this->success(__('Saved successfully')); } } diff --git a/app/admin/controller/game/Live.php b/app/admin/controller/game/Live.php index 8153437..5411bc4 100644 --- a/app/admin/controller/game/Live.php +++ b/app/admin/controller/game/Live.php @@ -71,9 +71,11 @@ class Live extends Backend $manualNumber = is_numeric((string) $manualRaw) ? (int) $manualRaw : null; $res = GameLiveService::calculateResult($recordId, $manualNumber); 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; $manualRaw = $request->post('manual_number'); if (!is_numeric((string) $manualRaw)) { - return $this->error('请填写开奖号码'); + return $this->error(__('Please enter the draw number')); } $manualNumber = (int) $manualRaw; $res = GameLiveService::scheduleDraw($recordId, $manualNumber); 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); } } diff --git a/app/admin/controller/game/ZiHuaDictionary.php b/app/admin/controller/game/ZiHuaDictionary.php index d14d5ed..b001d8a 100644 --- a/app/admin/controller/game/ZiHuaDictionary.php +++ b/app/admin/controller/game/ZiHuaDictionary.php @@ -4,6 +4,7 @@ namespace app\admin\controller\game; use app\common\controller\Backend; use app\common\library\game\ZiHuaDictionary as ZiHuaDictionaryLib; +use app\common\service\GameHotDataRedis; use InvalidArgumentException; use support\think\Db; use support\Response; @@ -111,6 +112,8 @@ class ZiHuaDictionary extends Backend return $this->error($e->getMessage()); } + GameHotDataRedis::gameConfigForget(ZiHuaDictionaryLib::CONFIG_KEY); + return $this->success(__('Saved successfully')); } } diff --git a/app/admin/library/Auth.php b/app/admin/library/Auth.php index 97c8e07..8cdd831 100644 --- a/app/admin/library/Auth.php +++ b/app/admin/library/Auth.php @@ -277,7 +277,30 @@ class Auth extends \ba\Auth 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> $menus + * @return array> + */ + 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 diff --git a/app/api/controller/Game.php b/app/api/controller/Game.php index 1069109..44f2950 100644 --- a/app/api/controller/Game.php +++ b/app/api/controller/Game.php @@ -6,9 +6,9 @@ namespace app\api\controller; use app\common\library\game\ZiHuaDictionary; use app\common\model\BetOrder; -use app\common\model\GameConfig; use app\common\model\GameRecord; use app\common\model\UserWalletRecord; +use app\common\service\GameHotDataRedis; use app\common\service\UserPushService; use support\think\Db; use Webman\Http\Request; @@ -25,15 +25,15 @@ class Game extends MobileBase return $response; } - $period = GameRecord::order('id', 'desc')->find(); + $periodRow = GameHotDataRedis::gameRecordLatest(); $now = time(); - $startAt = $period ? $this->intValue($period->period_start_at) : $now; + $startAt = $periodRow ? $this->intValue($periodRow['period_start_at'] ?? 0) : $now; $lockAt = $startAt + 20; $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(); - $dictionaryItems = ZiHuaDictionary::parseFromConfigValue($dictionaryConfig?->config_value ?? null); + $dictionaryConfig = GameHotDataRedis::gameConfigRow(ZiHuaDictionary::CONFIG_KEY) ?? []; + $dictionaryItems = ZiHuaDictionary::parseFromConfigValue($dictionaryConfig['config_value'] ?? null); $items = []; foreach ($dictionaryItems as $row) { $items[] = [ @@ -48,8 +48,8 @@ class Game extends MobileBase return $this->mobileSuccess([ 'server_time' => $now, 'period' => [ - 'period_no' => $period->period_no ?? '', - 'status' => $this->mapPeriodStatus($period->status ?? null), + 'period_no' => (string) ($periodRow['period_no'] ?? ''), + 'status' => $this->mapPeriodStatus($periodRow['status'] ?? null), 'countdown' => $countdown, 'lock_at' => $lockAt, 'open_at' => $openAt, @@ -73,8 +73,8 @@ class Game extends MobileBase if ($response !== null) { return $response; } - $dictionaryConfig = GameConfig::where('config_key', ZiHuaDictionary::CONFIG_KEY)->find(); - $dictionaryItems = ZiHuaDictionary::parseFromConfigValue($dictionaryConfig?->config_value ?? null); + $dictionaryConfig = GameHotDataRedis::gameConfigRow(ZiHuaDictionary::CONFIG_KEY) ?? []; + $dictionaryItems = ZiHuaDictionary::parseFromConfigValue($dictionaryConfig['config_value'] ?? null); $items = []; foreach ($dictionaryItems as $row) { $items[] = [ @@ -85,7 +85,7 @@ class Game extends MobileBase ]; } return $this->mobileSuccess([ - 'version' => (string) ($dictionaryConfig->update_time ?? '1'), + 'version' => (string) ($dictionaryConfig['update_time'] ?? '1'), 'items' => $items, ]); } @@ -118,19 +118,19 @@ class Game extends MobileBase if ($response !== null) { return $response; } - $period = GameRecord::order('id', 'desc')->find(); - if (!$period) { + $periodRow = GameHotDataRedis::gameRecordLatest(); + if (!$periodRow) { return $this->mobileError(2002, 'Game period does not exist'); } $now = time(); - $startAt = $this->intValue($period->period_start_at); + $startAt = $this->intValue($periodRow['period_start_at'] ?? 0); return $this->mobileSuccess([ - 'period_id' => $period->id, - 'period_no' => $period->period_no, - 'status' => $this->mapPeriodStatus($period->status), + 'period_id' => $periodRow['id'], + 'period_no' => $periodRow['period_no'], + 'status' => $this->mapPeriodStatus($periodRow['status'] ?? null), 'countdown' => max(0, ($startAt + 30) - $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(), ]); 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, [ 'order_no' => $orderNo, 'period_no' => (string) $period->period_no, @@ -355,7 +359,11 @@ class Game extends MobileBase 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 === '') { return $default; } diff --git a/app/common/library/Auth.php b/app/common/library/Auth.php index a7aa808..af19e2a 100644 --- a/app/common/library/Auth.php +++ b/app/common/library/Auth.php @@ -7,6 +7,7 @@ use ba\Random; use support\think\Db; use app\common\model\User; use app\common\facade\Token; +use app\common\service\GameHotDataRedis; use Webman\Http\Request; /** @@ -69,7 +70,7 @@ class Auth extends \ba\Auth Token::tokenExpirationCheck($tokenData); $userId = $tokenData['user_id']; if ($tokenData['type'] == self::TOKEN_TYPE && $userId > 0) { - $this->model = User::where('id', $userId)->find(); + $this->model = GameHotDataRedis::userModelFromCacheOrDb($userId); if (!$this->model) { $this->setError('Account not exist'); return false; diff --git a/app/common/library/finance/WithdrawFlow.php b/app/common/library/finance/WithdrawFlow.php index d1af828..469bbdb 100644 --- a/app/common/library/finance/WithdrawFlow.php +++ b/app/common/library/finance/WithdrawFlow.php @@ -4,7 +4,7 @@ declare(strict_types=1); 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 { - $row = Db::name('game_config')->where('config_key', self::CONFIG_KEY)->find(); + $row = GameHotDataRedis::gameConfigRow(self::CONFIG_KEY); if (!$row) { return self::DEFAULT_RATIO; } diff --git a/app/common/library/game/StreakWinReward.php b/app/common/library/game/StreakWinReward.php index e469823..5cc4813 100644 --- a/app/common/library/game/StreakWinReward.php +++ b/app/common/library/game/StreakWinReward.php @@ -4,7 +4,7 @@ declare(strict_types=1); namespace app\common\library\game; -use support\think\Db; +use app\common\service\GameHotDataRedis; /** * 连胜奖励(game_config.streak_win_reward):按「连胜档位」1~10 配置赔率系数与是否大奖。 @@ -100,7 +100,7 @@ final class StreakWinReward if (self::$cache !== null) { 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); return self::$cache; diff --git a/app/common/middleware/LoadLangPack.php b/app/common/middleware/LoadLangPack.php index 6e3e625..5b5c25c 100644 --- a/app/common/middleware/LoadLangPack.php +++ b/app/common/middleware/LoadLangPack.php @@ -23,33 +23,87 @@ class LoadLangPack implements MiddlewareInterface return $handler($request); } + /** + * 解析当前请求语言。 + * - 后台 admin:优先请求头 think-lang(zh-cn / en),其次 lang 头,再次查询/表单参数 lang(支持 zh→zh-cn)。 + * - 对外 api:优先查询/表单参数 lang(zh / 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 { - // 优先从请求头 lang / think-lang 获取前端选择的语言 - // 支持: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)); - } + $langSet = $this->resolveLangSet($request); // 设置当前请求的翻译语言,使 __() 和 trans() 使用正确的语言 if (function_exists('locale')) { @@ -74,6 +128,12 @@ class LoadLangPack implements MiddlewareInterface $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 等使用 // 同时加载到 messages 域,使 __() 能正确翻译控制器内的文案(如安装页错误提示) $controllerPath = get_controller_path($request); diff --git a/app/common/model/GameConfig.php b/app/common/model/GameConfig.php index 33a9e01..6833d12 100644 --- a/app/common/model/GameConfig.php +++ b/app/common/model/GameConfig.php @@ -2,6 +2,8 @@ namespace app\common\model; +use app\common\library\game\StreakWinReward; +use app\common\service\GameHotDataRedis; use support\think\Model; /** @@ -17,4 +19,26 @@ class GameConfig extends Model 'create_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(); + } + } } diff --git a/app/common/model/GameRecord.php b/app/common/model/GameRecord.php index 764f97a..69771b2 100644 --- a/app/common/model/GameRecord.php +++ b/app/common/model/GameRecord.php @@ -2,6 +2,7 @@ namespace app\common\model; +use app\common\service\GameHotDataRedis; use support\think\Model; class GameRecord extends Model @@ -41,4 +42,16 @@ class GameRecord extends Model } 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); + } } diff --git a/app/common/model/User.php b/app/common/model/User.php index bd65664..380d3d0 100644 --- a/app/common/model/User.php +++ b/app/common/model/User.php @@ -2,6 +2,7 @@ namespace app\common\model; +use app\common\service\GameHotDataRedis; use support\think\Model; /** @@ -62,4 +63,20 @@ class User extends Model { 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); + } + } } diff --git a/app/common/service/GameBetSettleService.php b/app/common/service/GameBetSettleService.php index 68dffd1..7c101c7 100644 --- a/app/common/service/GameBetSettleService.php +++ b/app/common/service/GameBetSettleService.php @@ -131,6 +131,7 @@ final class GameBetSettleService 'current_streak' => $next, 'update_time' => $now, ]); + GameHotDataRedis::userForget($userId); } foreach ($aggregateByUser as $userId => $agg) { @@ -272,6 +273,7 @@ final class GameBetSettleService 'bet_flow_coin' => Db::raw('bet_flow_coin + ' . $flow), 'update_time' => $now, ]); + GameHotDataRedis::userForget($userId); } /** @@ -319,6 +321,7 @@ final class GameBetSettleService 'coin' => $after, 'update_time' => $now, ]); + GameHotDataRedis::userForget($userId); return $after; } diff --git a/app/common/service/GameHotDataRedis.php b/app/common/service/GameHotDataRedis.php new file mode 100644 index 0000000..953007a --- /dev/null +++ b/app/common/service/GameHotDataRedis.php @@ -0,0 +1,279 @@ +|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|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|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|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|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) { + } + } +} diff --git a/app/common/service/GameRecordService.php b/app/common/service/GameRecordService.php index 27703a8..c9abad1 100644 --- a/app/common/service/GameRecordService.php +++ b/app/common/service/GameRecordService.php @@ -17,7 +17,7 @@ final class GameRecordService public static function getConfigBool(string $key): bool { - $row = Db::name('game_config')->where('config_key', $key)->find(); + $row = GameHotDataRedis::gameConfigRow($key); if (!$row) { return false; } @@ -99,6 +99,7 @@ final class GameRecordService 'create_time' => $now, 'update_time' => $now, ]); + GameHotDataRedis::gameRecordForget(); return $periodNo; } @@ -122,6 +123,7 @@ final class GameRecordService 'remark' => $remark, 'update_time' => $now, ]); + GameHotDataRedis::gameConfigForget($key); return; } Db::name('game_config')->insert([ @@ -132,5 +134,6 @@ final class GameRecordService 'create_time' => $now, 'update_time' => $now, ]); + GameHotDataRedis::gameConfigForget($key); } } diff --git a/app/common/service/GameRecordStatService.php b/app/common/service/GameRecordStatService.php index 7270288..b2cb2b1 100644 --- a/app/common/service/GameRecordStatService.php +++ b/app/common/service/GameRecordStatService.php @@ -33,6 +33,7 @@ final class GameRecordStatService 'winner_user_count' => 0, 'update_time' => $now, ]); + GameHotDataRedis::gameRecordForget($recordId); return; } @@ -78,6 +79,7 @@ final class GameRecordStatService 'winner_user_count' => count($winnerUserIds), 'update_time' => $now, ]); + GameHotDataRedis::gameRecordForget($recordId); } /**