From 0a3af2d4220b145d6d280f75bdb933e70992703a Mon Sep 17 00:00:00 2001 From: zhenhui <1276357500@qq.com> Date: Wed, 4 Mar 2026 17:06:30 +0800 Subject: [PATCH] =?UTF-8?q?[=E6=8E=A5=E5=8F=A3]=E6=96=B0=E5=A2=9E=E6=8A=BD?= =?UTF-8?q?=E5=A5=96=E6=8E=A5=E5=8F=A3/api/game/playStart?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dice/player_wallet_record/index/index.vue | 8 +- server/app/api/controller/GameController.php | 76 +++++++ server/app/api/logic/PlayStartLogic.php | 208 ++++++++++++++++++ server/app/api/service/LotteryService.php | 138 ++++++++++++ .../dice/model/play_record/DicePlayRecord.php | 1 + server/config/route.php | 1 + 6 files changed, 428 insertions(+), 4 deletions(-) create mode 100644 server/app/api/logic/PlayStartLogic.php create mode 100644 server/app/api/service/LotteryService.php diff --git a/saiadmin-artd/src/views/plugin/dice/player_wallet_record/index/index.vue b/saiadmin-artd/src/views/plugin/dice/player_wallet_record/index/index.vue index b1b9963..cdb6c5c 100644 --- a/saiadmin-artd/src/views/plugin/dice/player_wallet_record/index/index.vue +++ b/saiadmin-artd/src/views/plugin/dice/player_wallet_record/index/index.vue @@ -108,7 +108,7 @@ getData() } - // 类型展示:0=充值 1=提现 2=购买抽奖次数 3=管理员加点 4=管理员扣点 + // 类型展示:0=充值 1=提现 2=购买抽奖次数 3=管理员加点 4=管理员扣点 5=抽奖 const typeFormatter = (row: Record) => { const t = row.type if (t === 0) return '充值' @@ -116,18 +116,18 @@ if (t === 2) return '购买抽奖次数' if (t === 3) return '管理员加点' if (t === 4) return '管理员扣点' + if (t === 5) return '抽奖' return '-' } // 类型对应 tag 底色:0 充值 1 提现 2 购买 3 加点 4 扣点 - const typeTagType = ( - t: unknown - ): 'success' | 'warning' | 'danger' | 'info' | 'primary' => { + const typeTagType = (t: unknown): 'success' | 'warning' | 'danger' | 'info' | 'primary' => { if (t === 0) return 'success' if (t === 1) return 'warning' if (t === 2) return 'primary' if (t === 3) return 'success' if (t === 4) return 'danger' + if (t === 5) return 'info' return 'info' } diff --git a/server/app/api/controller/GameController.php b/server/app/api/controller/GameController.php index 9103682..dcd31ff 100644 --- a/server/app/api/controller/GameController.php +++ b/server/app/api/controller/GameController.php @@ -7,9 +7,13 @@ use support\Request; use support\Response; use app\api\logic\UserLogic; use app\api\logic\GameLogic; +use app\api\logic\PlayStartLogic; use app\api\util\ReturnCode; +use app\dice\model\play_record\DicePlayRecord; +use app\dice\model\player\DicePlayer; use app\dice\model\reward_config\DiceRewardConfig; use plugin\saiadmin\basic\OpenController; +use plugin\saiadmin\exception\ApiException; /** * 游戏相关接口(购买抽奖券等) @@ -65,4 +69,76 @@ class GameController extends OpenController $list = DiceRewardConfig::order('id', 'asc')->select()->toArray(); return $this->success($list); } + + /** + * 开始游戏(抽奖一局) + * POST /api/game/playStart + * header: auth-token, user-token + * body: rediction 必传,0=无 1=中奖 + * 余额不足时返回 code=200、message=玩家当前余额不足无法开启对局;超时返回 code=200、message=服务超时,并记录 status=0 + */ + public function playStart(Request $request): Response + { + $token = $request->header('user-token'); + if (empty($token)) { + $auth = $request->header('authorization'); + if ($auth && stripos($auth, 'Bearer ') === 0) { + $token = trim(substr($auth, 7)); + } + } + if (empty($token)) { + return $this->fail('请携带 user-token', ReturnCode::MISSING_TOKEN); + } + $userId = UserLogic::getUserIdFromToken($token); + if ($userId === null) { + return $this->fail('user-token 无效或已过期', ReturnCode::TOKEN_TIMEOUT); + } + + $rediction = $request->post('rediction'); + if ($rediction === '' || $rediction === null) { + return $this->fail('请传递 rediction 参数', ReturnCode::EMPTY_PARAMS); + } + $direction = (int) $rediction; + if (!in_array($direction, [0, 1], true)) { + return $this->fail('rediction 必须为 0 或 1', ReturnCode::EMPTY_PARAMS); + } + + $player = DicePlayer::find($userId); + if (!$player) { + return $this->fail('用户不存在', ReturnCode::EMPTY_PARAMS); + } + $minEv = (float) DiceRewardConfig::min('real_ev'); + $minCoin = abs($minEv + 100); + $coin = (float) $player->coin; + if ($coin < $minCoin) { + return $this->success([], '当前玩家余额小于DiceRewardConfigMin.real_ev+100无法继续游戏'); + } + + try { + $logic = new PlayStartLogic(); + $data = $logic->run($userId, $direction); + return $this->success($data); + } catch (ApiException $e) { + return $this->fail($e->getMessage(), ReturnCode::EMPTY_PARAMS); + } catch (\Throwable $e) { + $timeoutRecord = null; + try { + $timeoutRecord = DicePlayRecord::create([ + 'player_id' => $userId, + 'lottery_config_id' => 0, + 'lottery_type' => 0, + 'win_coin' => 0, + 'direction' => $direction, + 'reward_config_id' => 0, + 'start_index' => 0, + 'target_index' => 0, + 'roll_array' => '[]', + 'status' => PlayStartLogic::RECORD_STATUS_TIMEOUT, + ]); + } catch (\Throwable $_) { + } + $payload = $timeoutRecord ? ['record' => $timeoutRecord->toArray()] : []; + return $this->success($payload, '服务超时'); + } + } } diff --git a/server/app/api/logic/PlayStartLogic.php b/server/app/api/logic/PlayStartLogic.php new file mode 100644 index 0000000..0c13b59 --- /dev/null +++ b/server/app/api/logic/PlayStartLogic.php @@ -0,0 +1,208 @@ +coin; + if ($coin < $minCoin) { + throw new ApiException('当前玩家余额小于DiceRewardConfigMin.real_ev+100无法继续游戏'); + } + + $paid = (int) ($player->paid_draw_count ?? 0); + $free = (int) ($player->free_draw_count ?? 0); + if ($paid + $free <= 0) { + throw new ApiException('抽奖券不足'); + } + + $lotteryService = LotteryService::getOrCreate($playerId); + $ticketType = LotteryService::drawTicketType($paid, $free); + $config = $ticketType === self::LOTTERY_TYPE_PAID + ? ($lotteryService->getConfigType0Id() ? DiceLotteryConfig::find($lotteryService->getConfigType0Id()) : null) + : ($lotteryService->getConfigType1Id() ? DiceLotteryConfig::find($lotteryService->getConfigType1Id()) : null); + if (!$config) { + throw new ApiException('奖池配置不存在'); + } + + $tier = LotteryService::drawTierByWeights($config); + $rewards = DiceRewardConfig::where('tier', $tier)->select(); + if ($rewards->isEmpty()) { + throw new ApiException('该档位暂无奖励配置'); + } + $rewardList = $rewards->all(); + $reward = $rewardList[array_rand($rewardList)]; + $realEv = (float) $reward->real_ev; + $winCoin = 100 + $realEv; // 赢取平台币 = 100 + DiceRewardConfig.real_ev + $gridNumber = (int) $reward->grid_number; + $startIndex = (int) Cache::get(LotteryService::getStartIndexKey($playerId), 0); + $targetIndex = (int) $reward->id; + $rollArray = $this->generateRollArray($gridNumber); + + $record = null; + $configId = (int) $config->id; + $rewardId = (int) $reward->id; + $configName = (string) ($config->name ?? ''); + try { + Db::transaction(function () use ( + $playerId, + $configId, + $rewardId, + $configName, + $ticketType, + $winCoin, + $realEv, + $direction, + $startIndex, + $targetIndex, + $rollArray, + &$record + ) { + $record = DicePlayRecord::create([ + 'player_id' => $playerId, + 'lottery_config_id' => $configId, + 'lottery_type' => $ticketType, + 'win_coin' => $winCoin, + 'direction' => $direction, + 'reward_config_id' => $rewardId, + 'start_index' => $startIndex, + 'target_index' => $targetIndex, + 'roll_array' => is_array($rollArray) ? json_encode($rollArray) : $rollArray, + 'lottery_name' => $configName, + 'status' => self::RECORD_STATUS_SUCCESS, + ]); + + $p = DicePlayer::find($playerId); + if (!$p) { + throw new \RuntimeException('玩家不存在'); + } + $coinBefore = (float) $p->coin; + $coinAfter = $coinBefore + $winCoin; + $p->coin = $coinAfter; + $p->total_draw_count = max(0, (int) $p->total_draw_count - 1); + if ($ticketType === self::LOTTERY_TYPE_PAID) { + $p->paid_draw_count = max(0, (int) $p->paid_draw_count - 1); + } else { + $p->free_draw_count = max(0, (int) $p->free_draw_count - 1); + } + $p->save(); + + // 累加彩金池盈利额度(累加值为 -real_ev)。若 dice_lottery_config 表有 ev 字段则执行 + try { + DiceLotteryConfig::where('id', $configId)->update([ + 'ev' => Db::raw('IFNULL(ev,0) - ' . (float) $realEv), + ]); + } catch (\Throwable $_) { + } + + DicePlayerWalletRecord::create([ + 'player_id' => $playerId, + 'coin' => $winCoin, + 'type' => self::WALLET_TYPE_DRAW, + 'wallet_before' => $coinBefore, + 'wallet_after' => $coinAfter, + 'remark' => '抽奖|play_record_id=' . $record->id, + ]); + + Cache::set(LotteryService::getStartIndexKey($playerId), $targetIndex, 86400 * 30); + }); + } catch (\Throwable $e) { + if ($record === null) { + try { + $record = DicePlayRecord::create([ + 'player_id' => $playerId, + 'lottery_config_id' => $configId ?? 0, + 'lottery_type' => $ticketType, + 'win_coin' => 0, + 'direction' => $direction, + 'reward_config_id' => 0, + 'start_index' => $startIndex, + 'target_index' => 0, + 'roll_array' => '[]', + 'status' => self::RECORD_STATUS_TIMEOUT, + ]); + } catch (\Throwable $_) { + // 表可能无 status 字段时忽略 + } + } + throw $e; + } + + $updated = DicePlayer::find($playerId); + if ($updated) { + UserCache::setUser($playerId, $updated->hidden(['password'])->toArray()); + } + + if (!$record instanceof DicePlayRecord) { + throw new \RuntimeException('对局记录创建失败'); + } + $arr = $record->toArray(); + if (isset($arr['roll_array']) && is_string($arr['roll_array'])) { + $arr['roll_array'] = json_decode($arr['roll_array'], true) ?? []; + } + return $arr; + } + + /** 生成 5 个 1-6 的点数,和为 grid_number(5~30),严格不超范围 */ + private function generateRollArray(int $gridNumber): array + { + $minSum = 5; + $maxSum = 30; + $n = max($minSum, min($maxSum, $gridNumber)); + $dice = [1, 1, 1, 1, 1]; + $remain = $n - 5; + while ($remain > 0) { + $i = array_rand($dice); + if ($dice[$i] < 6) { + $add = min($remain, 6 - $dice[$i]); + $dice[$i] += $add; + $remain -= $add; + } + } + shuffle($dice); + return $dice; + } +} diff --git a/server/app/api/service/LotteryService.php b/server/app/api/service/LotteryService.php new file mode 100644 index 0000000..9b4f0cd --- /dev/null +++ b/server/app/api/service/LotteryService.php @@ -0,0 +1,138 @@ +playerId = $playerId; + } + + public static function getRedisKey(int $playerId): string + { + return self::REDIS_KEY_PREFIX . $playerId; + } + + public static function getStartIndexKey(int $playerId): string + { + return self::REDIS_KEY_START_INDEX . $playerId; + } + + /** 从 Redis 加载或根据玩家与 DiceLotteryConfig 创建并保存 */ + public static function getOrCreate(int $playerId): self + { + $key = self::getRedisKey($playerId); + $cached = Cache::get($key); + if ($cached && is_string($cached)) { + $data = json_decode($cached, true); + if (is_array($data)) { + $s = new self($playerId); + $s->configType0Id = (int) ($data['config_type_0_id'] ?? 0); + $s->configType1Id = (int) ($data['config_type_1_id'] ?? 0); + $s->playerWeights = $data['player_weights'] ?? []; + return $s; + } + } + $player = DicePlayer::find($playerId); + if (!$player) { + throw new \RuntimeException('玩家不存在'); + } + $config0 = DiceLotteryConfig::where('type', 0)->find(); + $config1 = DiceLotteryConfig::where('type', 1)->find(); + $s = new self($playerId); + $s->configType0Id = $config0 ? (int) $config0->id : null; + $s->configType1Id = $config1 ? (int) $config1->id : null; + $s->playerWeights = [ + 't1_wight' => (int) ($player->t1_wight ?? 0), + 't2_wight' => (int) ($player->t2_wight ?? 0), + 't3_wight' => (int) ($player->t3_wight ?? 0), + 't4_wight' => (int) ($player->t4_wight ?? 0), + 't5_wight' => (int) ($player->t5_wight ?? 0), + ]; + $s->save(); + return $s; + } + + public function save(): void + { + $key = self::getRedisKey($this->playerId); + $data = [ + 'config_type_0_id' => $this->configType0Id, + 'config_type_1_id' => $this->configType1Id, + 'player_weights' => $this->playerWeights, + ]; + Cache::set($key, json_encode($data), self::EXPIRE); + } + + /** 根据奖池配置的 t1_wight..t5_wight 权重随机抽取档位 T1-T5 */ + public static function drawTierByWeights(DiceLotteryConfig $config): string + { + $tiers = ['T1', 'T2', 'T3', 'T4', 'T5']; + $weights = [ + (int) ($config->t1_wight ?? 0), + (int) ($config->t2_wight ?? 0), + (int) ($config->t3_wight ?? 0), + (int) ($config->t4_wight ?? 0), + (int) ($config->t5_wight ?? 0), + ]; + $total = array_sum($weights); + if ($total <= 0) { + return $tiers[array_rand($tiers)]; + } + $r = mt_rand(1, $total); + $acc = 0; + foreach ($weights as $i => $w) { + $acc += $w; + if ($r <= $acc) { + return $tiers[$i]; + } + } + return $tiers[4]; + } + + /** 按 paid_draw_count 与 free_draw_count 权重随机抽取 0=付费 1=免费 */ + public static function drawTicketType(int $paid, int $free): int + { + if ($paid <= 0 && $free <= 0) { + throw new \RuntimeException('抽奖券不足'); + } + if ($paid <= 0) { + return 1; + } + if ($free <= 0) { + return 0; + } + $total = $paid + $free; + $r = mt_rand(1, $total); + return $r <= $paid ? 0 : 1; + } + + public function getConfigType0Id(): ?int + { + return $this->configType0Id; + } + + public function getConfigType1Id(): ?int + { + return $this->configType1Id; + } +} diff --git a/server/app/dice/model/play_record/DicePlayRecord.php b/server/app/dice/model/play_record/DicePlayRecord.php index d942c57..60464d8 100644 --- a/server/app/dice/model/play_record/DicePlayRecord.php +++ b/server/app/dice/model/play_record/DicePlayRecord.php @@ -30,6 +30,7 @@ use think\model\relation\BelongsTo; * @property $target_index 结束索引 * @property $roll_array 摇取点数,格式:[1,2,3,4,5](5个点数) * @property $lottery_name 奖池名 + * @property $status 状态:0=超时/失败 1=成功 * @property $create_time 创建时间 * @property $update_time 修改时间 */ diff --git a/server/config/route.php b/server/config/route.php index 0d3c4c3..21dec60 100644 --- a/server/config/route.php +++ b/server/config/route.php @@ -25,6 +25,7 @@ Route::group('/api', function () { Route::get('/user/balance', [app\api\controller\UserController::class, 'balance']); Route::post('/game/buyLotteryTickets', [app\api\controller\GameController::class, 'buyLotteryTickets']); Route::get('/game/lotteryPool', [app\api\controller\GameController::class, 'lotteryPool']); + Route::post('/game/playStart', [app\api\controller\GameController::class, 'playStart']); })->middleware([CheckApiAuthMiddleware::class]);