Files
webman-buildadmin/app/common/service/GameHotDataRedis.php
zhenhui 687257adaa 1.优化实时对局页面样式以及自动创建下一局和作废本局的记录
2.新增派彩达到game_config.jackpot_max_amount必须审核才能发放
3.新增游戏对局记录-查看游玩记录btn
3.备份MySQL数据库
2026-04-28 18:25:44 +08:00

382 lines
12 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
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:';
/**
* 进程内实例化缓存(避免同一请求/进程内重复打 Redis/DB
* - key => null表示已确认不存在负缓存
* - key => array表示已加载行数据
*
* @var array<string, array<string, mixed>|null>
*/
private static array $gcLocal = [];
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;
}
// game_config 为全局配置,多进程/多 worker 间必须强一致;
// 因此不使用进程内本地缓存gcLocal避免某个进程读到旧值导致前端回弹/行为冲突。
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);
unset(self::$gcLocal[$configKey]);
}
/**
* 按库中当前行覆盖 game_config 缓存(无行则删缓存)
*/
public static function gameConfigReplaceFromDb(string $configKey): void
{
if ($configKey === '') {
return;
}
// 无论是否启用 Redis 热点缓存,都要刷新进程内缓存,避免同一 worker 读到旧值
$row = Db::name('game_config')->where('config_key', $configKey)->find();
if (!$row) {
self::$gcLocal[$configKey] = null;
if (self::enabled()) {
self::redisDel(self::KEY_GC . $configKey);
}
return;
}
self::$gcLocal[$configKey] = $row;
if (self::enabled()) {
$ttl = self::intConfig('ttl_game_config', 86400);
self::redisSetEx(self::KEY_GC . $configKey, $ttl, json_encode($row, JSON_UNESCAPED_UNICODE));
}
}
/**
* 对局写入后:刷新指定 id 的行缓存,并删除「活跃局 / 最新局」聚合键以免脏读
*
* @param int|null $id 可为 null仅清聚合键
*/
public static function gameRecordSyncCachesAfterDbWrite(?int $id): void
{
if (!self::enabled()) {
return;
}
if ($id !== null && $id > 0) {
$row = Db::name('game_record')->where('id', $id)->find();
if ($row) {
$ttl = self::intConfig('ttl_game_record', 60);
self::redisSetEx(self::KEY_GR_ID . $id, $ttl, json_encode($row, JSON_UNESCAPED_UNICODE));
} else {
self::redisDel(self::KEY_GR_ID . $id);
}
}
self::redisDel(self::KEY_GR_ACTIVE, self::KEY_GR_LATEST);
}
/**
* @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 行并覆盖写入 Redis与 DB 同事务后调用,保持缓存与库一致)
*/
public static function userReplaceCacheFromDb(int $userId): void
{
if ($userId <= 0 || !self::enabled()) {
return;
}
$row = Db::name('user')->where('id', $userId)->find();
if (!$row) {
self::userForget($userId);
return;
}
$ttl = self::intConfig('ttl_user', 90);
self::redisSetEx(self::KEY_USER . $userId, $ttl, json_encode($row, JSON_UNESCAPED_UNICODE));
}
/**
* 尝试获取「后台修改该用户」互斥锁Redis SET NX EX
* 与热点缓存开关无关:只要 Redis 可用即加锁连接失败时降级为仅依赖数据库乐观锁WHERE coin=)。
*
* @return array{acquired: bool, token: ?string, redis_lock: bool}
*/
public static function userAdminMutationLockTry(int $userId): array
{
if ($userId <= 0) {
return ['acquired' => false, 'token' => null, 'redis_lock' => false];
}
return GameHotDataLock::tryAcquire(GameHotDataLock::TYPE_USER, (string) $userId);
}
/**
* 释放 userAdminMutationLockTry 取得的锁(仅当 redis_lock 且 token 非空)
*/
public static function userAdminMutationLockRelease(int $userId, ?string $token, bool $redisLock): void
{
if ($userId <= 0) {
return;
}
GameHotDataLock::release(GameHotDataLock::TYPE_USER, (string) $userId, $token, $redisLock);
}
/**
* 用缓存行构造已存在库的 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) {
}
}
}