From 6b9fb0c26e847f87a82eca91af688f821da2e56b Mon Sep 17 00:00:00 2001 From: zhenhui <1276357500@qq.com> Date: Wed, 25 Mar 2026 15:51:50 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E6=B8=B8=E7=8E=A9=E6=96=B9?= =?UTF-8?q?=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../locales/langs/en/dice/play_record.json | 2 + .../locales/langs/zh/dice/play_record.json | 2 + .../plugin/dice/play_record/index/index.vue | 2 + server/app/api/controller/GameController.php | 16 ++-- server/app/api/logic/GameLogic.php | 1 + server/app/api/logic/PlayStartLogic.php | 93 +++++++++++++------ .../play_record/DicePlayRecordController.php | 6 +- .../dice/model/play_record/DicePlayRecord.php | 3 + .../DicePlayerTicketRecord.php | 1 + ...e_play_record_add_ante_and_paid_amount.sql | 4 + .../db/dice_player_ticket_record_add_ante.sql | 3 + 11 files changed, 96 insertions(+), 37 deletions(-) create mode 100644 server/db/dice_play_record_add_ante_and_paid_amount.sql create mode 100644 server/db/dice_player_ticket_record_add_ante.sql diff --git a/saiadmin-artd/src/locales/langs/en/dice/play_record.json b/saiadmin-artd/src/locales/langs/en/dice/play_record.json index 8212ca1..72cc7b0 100644 --- a/saiadmin-artd/src/locales/langs/en/dice/play_record.json +++ b/saiadmin-artd/src/locales/langs/en/dice/play_record.json @@ -64,6 +64,8 @@ "player": "Player", "lotteryPoolConfig": "Lottery Pool Config", "drawType": "Draw Type", + "ante": "Ante", + "paidAmount": "Paid Amount", "isBigWin": "Is Big Win", "winCoin": "Win Coin", "superWinCoin": "Super Win Coin", diff --git a/saiadmin-artd/src/locales/langs/zh/dice/play_record.json b/saiadmin-artd/src/locales/langs/zh/dice/play_record.json index e986edc..cdf41df 100644 --- a/saiadmin-artd/src/locales/langs/zh/dice/play_record.json +++ b/saiadmin-artd/src/locales/langs/zh/dice/play_record.json @@ -64,6 +64,8 @@ "player": "玩家", "lotteryPoolConfig": "彩金池配置", "drawType": "抽奖类型", + "ante": "注数", + "paidAmount": "付费金额", "isBigWin": "是否中大奖", "winCoin": "赢取平台币", "superWinCoin": "中大奖平台币", diff --git a/saiadmin-artd/src/views/plugin/dice/play_record/index/index.vue b/saiadmin-artd/src/views/plugin/dice/play_record/index/index.vue index 061144a..4756abe 100644 --- a/saiadmin-artd/src/views/plugin/dice/play_record/index/index.vue +++ b/saiadmin-artd/src/views/plugin/dice/play_record/index/index.vue @@ -199,6 +199,8 @@ useSlot: true }, { prop: 'lottery_type', label: 'page.table.drawType', width: 100, useSlot: true }, + { prop: 'ante', label: 'page.table.ante', width: 80, align: 'center' }, + { prop: 'paid_amount', label: 'page.table.paidAmount', width: 110, align: 'center' }, { prop: 'is_win', label: 'page.table.isBigWin', width: 100, useSlot: true }, { prop: 'win_coin', label: 'page.table.winCoin', width: 110 }, { prop: 'super_win_coin', label: 'page.table.superWinCoin', width: 120 }, diff --git a/server/app/api/controller/GameController.php b/server/app/api/controller/GameController.php index 8701ffc..7f4755f 100644 --- a/server/app/api/controller/GameController.php +++ b/server/app/api/controller/GameController.php @@ -162,21 +162,21 @@ class GameController extends BaseController if ($direction !== null) { $direction = (int) $direction; } + $ante = $request->post('ante'); + if ($ante !== null) { + $ante = (int) $ante; + } if (!in_array($direction, [0, 1], true)) { return $this->fail('direction must be 0 or 1', ReturnCode::PARAMS_ERROR); } + if (!is_int($ante) || $ante <= 0) { + return $this->fail('ante must be a positive integer', ReturnCode::PARAMS_ERROR); + } $player = DicePlayer::find($userId); if (!$player) { return $this->fail('User not found', ReturnCode::NOT_FOUND); } - $minEv = DiceRewardConfig::getCachedMinRealEv(); - $minCoin = abs($minEv + 100); - $coin = (float) $player->coin; - if ($coin < $minCoin) { - $msg = ApiLang::translateParams('Balance %s is less than %s, cannot continue', [$coin, $minCoin], $request); - return $this->success([], $msg); - } $lockName = 'play_start_' . $userId; $lockResult = Db::query('SELECT GET_LOCK(?, 30) as l', [$lockName]); @@ -185,7 +185,7 @@ class GameController extends BaseController } try { $logic = new PlayStartLogic(); - $data = $logic->run($userId, (int)$direction); + $data = $logic->run($userId, (int) $direction, $ante); $lang = $request->header('lang', 'zh'); if (!is_string($lang) || $lang === '') { diff --git a/server/app/api/logic/GameLogic.php b/server/app/api/logic/GameLogic.php index b68a8ad..9dd3a8d 100644 --- a/server/app/api/logic/GameLogic.php +++ b/server/app/api/logic/GameLogic.php @@ -108,6 +108,7 @@ class GameLogic 'player_id' => $playerId, 'admin_id' => $adminId, 'use_coins' => $cost, + 'ante' => 1, 'total_ticket_count' => $addTotal, 'paid_ticket_count' => $addPaid, 'free_ticket_count' => $addFree, diff --git a/server/app/api/logic/PlayStartLogic.php b/server/app/api/logic/PlayStartLogic.php index c328e03..5a10c61 100644 --- a/server/app/api/logic/PlayStartLogic.php +++ b/server/app/api/logic/PlayStartLogic.php @@ -8,6 +8,7 @@ use app\api\util\ApiLang; use app\api\service\LotteryService; use app\dice\model\lottery_pool_config\DiceLotteryPoolConfig; use app\dice\model\play_record\DicePlayRecord; +use app\dice\model\ante_config\DiceAnteConfig; use app\dice\model\player\DicePlayer; use app\dice\model\player_ticket_record\DicePlayerTicketRecord; use app\dice\model\player_wallet_record\DicePlayerWalletRecord; @@ -34,8 +35,12 @@ class PlayStartLogic /** 对局状态:超时/失败 */ public const RECORD_STATUS_TIMEOUT = 0; - /** 开启对局最低余额 = |DiceRewardConfig 最小 real_ev + 100| */ - private const MIN_COIN_EXTRA = 100; + /** 单注费用(对应原票价 100) */ + private const UNIT_COST = 100; + /** 免费抽奖注数缓存 key 前缀(用于强制下一局注数一致) */ + private const FREE_ANTE_KEY_PREFIX = 'api:game:free_ante:'; + /** 免费抽奖注数缓存过期(秒) */ + private const FREE_ANTE_TTL = 86400 * 7; /** 豹子号中大奖额外平台币(无 BIGWIN 配置时兜底) */ private const SUPER_WIN_BONUS = 500; /** 可触发超级大奖的 grid_number(5=全1 10=全2 15=全3 20=全4 25=全5 30=全6) */ @@ -47,36 +52,60 @@ class PlayStartLogic * 执行一局游戏 * @param int $playerId 玩家ID * @param int $direction 方向 0=无/顺时针 1=中奖/逆时针(前端 direction) + * @param int $ante 注数(必须在 DiceAnteConfig.mult 中存在) * @return array 成功返回 DicePlayRecord 数据;余额不足时抛 ApiException,message 为约定文案 */ - public function run(int $playerId, int $direction): array + public function run(int $playerId, int $direction, int $ante): array { $player = DicePlayer::find($playerId); if (!$player) { throw new ApiException('User not found'); } - $minEv = DiceRewardConfig::getCachedMinRealEv(); - $minCoin = abs($minEv + self::MIN_COIN_EXTRA); $coin = (float) $player->coin; - if ($coin < $minCoin) { - throw new ApiException(ApiLang::translateParams('当前玩家余额%s小于%s无法继续游戏', [$coin, $minCoin])); + if ($ante <= 0) { + throw new ApiException('ante must be a positive integer'); } - $paid = (int) ($player->paid_ticket_count ?? 0); - $free = (int) ($player->free_ticket_count ?? 0); - if ($paid + $free <= 0) { - throw new ApiException('Insufficient lottery tickets'); + // 注数合规校验:ante 必须存在于 dice_ante_config.mult + $anteConfigModel = new DiceAnteConfig(); + $exists = $anteConfigModel->where('mult', $ante)->count(); + if ($exists <= 0) { + throw new ApiException('当前注数不合规,请选择正确的注数'); + } + + // 免费抽奖:不再使用抽奖券作为开始条件,仅用 free_ticket_count 表示“免费抽奖次数” + $freeCount = (int) ($player->free_ticket_count ?? 0); + $isFree = $freeCount > 0; + $ticketType = $isFree ? self::LOTTERY_TYPE_FREE : self::LOTTERY_TYPE_PAID; + + // 若为免费抽奖:注数必须与上一次触发免费抽奖时的注数一致 + if ($isFree) { + $requiredAnte = Cache::get(self::FREE_ANTE_KEY_PREFIX . $playerId); + if ($requiredAnte !== null && $requiredAnte !== '' && (int) $requiredAnte !== $ante) { + throw new ApiException('免费抽奖注数必须与上一次一致,请修改注数后继续'); + } } - $lotteryService = LotteryService::getOrCreate($playerId); - $ticketType = LotteryService::drawTicketType($paid, $free); $configType0 = DiceLotteryPoolConfig::where('name', 'default')->find(); $configType1 = DiceLotteryPoolConfig::where('name', 'killScore')->find(); if (!$configType0) { throw new ApiException('Lottery pool config not found (name=default required)'); } + // 余额校验:统一校验 ante * min(real_ev) + $minEv = DiceRewardConfig::getCachedMinRealEv(); + $needMinBalance = abs((float) $minEv) * $ante; + if ($coin < $needMinBalance) { + throw new ApiException('未达抽奖余额 ' . $needMinBalance . ',无法开始游戏'); + } + + // 付费抽奖:开始前扣除费用 ante * 100,不足则提示余额不足 + $paidAmount = $ticketType === self::LOTTERY_TYPE_PAID ? ($ante * self::UNIT_COST) : 0; + if ($ticketType === self::LOTTERY_TYPE_PAID && $coin < $paidAmount) { + throw new ApiException('余额不足'); + } + // 彩金池累计盈利:用于判断是否触发杀分(不再依赖单个玩家累计盈利) // 该值来自 dice_lottery_pool_config.profit_amount $poolProfitTotal = $configType0->profit_amount ?? 0; @@ -133,7 +162,8 @@ class PlayStartLogic $rollNumber = (int) ($chosen['grid_number'] ?? 0); $realEv = (float) ($chosen['real_ev'] ?? 0); $isTierT5 = (string) ($chosen['tier'] ?? '') === 'T5'; - $rewardWinCoin = $isTierT5 ? $realEv : (100 + $realEv); + // 玩家始终增加:(100 + real_ev) * ante(费用已在开始前扣除;免费抽奖同样按该口径结算) + $rewardWinCoin = (self::UNIT_COST + $realEv) * $ante; // 豹子判定:5/30 必豹子;10/15/20/25 按 DiceRewardConfig 中 BIGWIN 该点数的 weight 判定(0-10000,10000=100%) // 杀分档位:不触发豹子,5/30 已在上方抽取时排除,10/15/20/25 仅生成非豹子组合 @@ -166,7 +196,8 @@ class PlayStartLogic if ($doSuperWin) { $rollArray = $this->getSuperWinRollArray($rollNumber); $isWin = 1; - $superWinCoin = $bigWinRealEv > 0 ? $bigWinRealEv : self::SUPER_WIN_BONUS; + $bigWinEv = $bigWinRealEv > 0 ? $bigWinRealEv : self::SUPER_WIN_BONUS; + $superWinCoin = (self::UNIT_COST + $bigWinEv) * $ante; // 中 BIGWIN 豹子:不走原奖励流程,不记录原奖励,不触发 T5 再来一次,仅发放豹子奖金 $rewardWinCoin = 0; $realEv = 0; @@ -203,6 +234,8 @@ class PlayStartLogic $rewardId, $configName, $ticketType, + $ante, + $paidAmount, $winCoin, $superWinCoin, $rewardWinCoin, @@ -221,11 +254,13 @@ class PlayStartLogic 'admin_id' => $adminId, 'lottery_config_id' => $configId, 'lottery_type' => $ticketType, + 'ante' => $ante, + 'paid_amount' => $paidAmount, 'is_win' => $isWin, 'win_coin' => $winCoin, 'super_win_coin' => $superWinCoin, 'reward_win_coin' => $rewardWinCoin, - 'use_coins' => 0, + 'use_coins' => $paidAmount, 'direction' => $direction, 'reward_config_id' => $rewardId, 'start_index' => $startIndex, @@ -241,34 +276,40 @@ class PlayStartLogic throw new \RuntimeException('玩家不存在'); } $coinBefore = (float) $p->coin; - $coinAfter = $coinBefore + $winCoin; + // 开始前先扣付费金额,再加中奖金额(免费抽奖 paid_amount=0) + $coinAfter = $coinBefore - $paidAmount + $winCoin; $p->coin = $coinAfter; - $p->total_ticket_count = max(0, (int) $p->total_ticket_count - 1); - if ($ticketType === self::LOTTERY_TYPE_PAID) { - $p->paid_ticket_count = max(0, (int) $p->paid_ticket_count - 1); - } else { + // 不再使用抽奖券作为抽奖条件:付费不扣抽奖次数;免费抽奖仅消耗 free_ticket_count + if ($ticketType === self::LOTTERY_TYPE_FREE) { $p->free_ticket_count = max(0, (int) $p->free_ticket_count - 1); } // 若本局中奖档位为 T5,则额外赠送 1 次免费抽奖次数(总次数也 +1),并记录抽奖券获取记录 if ($isTierT5) { $p->free_ticket_count = (int) $p->free_ticket_count + 1; - $p->total_ticket_count = (int) $p->total_ticket_count + 1; DicePlayerTicketRecord::create([ 'player_id' => $playerId, 'admin_id' => $adminId, + 'ante' => $ante, 'free_ticket_count' => 1, 'remark' => '中奖结果为T5', ]); + // 记录免费抽奖注数,用于强制下一局注数一致 + Cache::set(self::FREE_ANTE_KEY_PREFIX . $playerId, $ante, self::FREE_ANTE_TTL); + } else { + // 若本次消耗了最后一次免费抽奖,则清理注数锁 + if ($ticketType === self::LOTTERY_TYPE_FREE && (int) $p->free_ticket_count <= 0) { + Cache::delete(self::FREE_ANTE_KEY_PREFIX . $playerId); + } } $p->save(); // 彩金池累计盈利累加在 name=default 彩金池上: - // 付费券:每局按“当前中奖金额(含 BIGWIN) - 抽奖券费用 100” + // 付费:每局按“当前中奖金额(含 BIGWIN) - 抽奖费用(ante*100)” // 免费券:取消票价成本 100,只计入中奖金额 - $perPlayProfit = ($ticketType === self::LOTTERY_TYPE_PAID) ? ($winCoin - 100.0) : $winCoin; + $perPlayProfit = ($ticketType === self::LOTTERY_TYPE_PAID) ? ($winCoin - (float) $paidAmount) : $winCoin; $addProfit = $perPlayProfit; try { DiceLotteryPoolConfig::where('id', $type0ConfigId)->update([ @@ -285,7 +326,8 @@ class PlayStartLogic DicePlayerWalletRecord::create([ 'player_id' => $playerId, 'admin_id' => $adminId, - 'coin' => $winCoin, + // 钱包流水记录本局净变化:-付费金额 + 中奖金额(免费抽奖付费金额为 0) + 'coin' => $winCoin - (float) $paidAmount, 'type' => self::WALLET_TYPE_DRAW, 'wallet_before' => $coinBefore, 'wallet_after' => $coinAfter, @@ -336,7 +378,6 @@ class PlayStartLogic $arr['tier'] = $tier ?? ''; // 记录完数据后返回当前玩家余额与抽奖次数 $arr['coin'] = $updated ? (float) $updated->coin : 0; - $arr['total_ticket_count'] = $updated ? (int) $updated->total_ticket_count : 0; return $arr; } diff --git a/server/app/dice/controller/play_record/DicePlayRecordController.php b/server/app/dice/controller/play_record/DicePlayRecordController.php index a63cc92..5589db1 100644 --- a/server/app/dice/controller/play_record/DicePlayRecordController.php +++ b/server/app/dice/controller/play_record/DicePlayRecordController.php @@ -64,9 +64,9 @@ class DicePlayRecordController extends BaseController // 按当前筛选条件统计:平台总盈利 = 付费抽奖(lottery_type=0)次数×100 - 玩家总收益(win_coin 求和) $sumQuery = clone $query; $playerTotalWin = (float) $sumQuery->sum('win_coin'); - $paidCountQuery = clone $query; - $paidCount = (int) $paidCountQuery->where('lottery_type', 0)->count(); - $totalWinCoin = $paidCount * 100 - $playerTotalWin; + $paidAmountQuery = clone $query; + $paidAmount = (float) $paidAmountQuery->where('lottery_type', 0)->sum('paid_amount'); + $totalWinCoin = $paidAmount - $playerTotalWin; $data = $this->logic->getList($query); $data['total_win_coin'] = $totalWinCoin; diff --git a/server/app/dice/model/play_record/DicePlayRecord.php b/server/app/dice/model/play_record/DicePlayRecord.php index 4d012d5..601bc96 100644 --- a/server/app/dice/model/play_record/DicePlayRecord.php +++ b/server/app/dice/model/play_record/DicePlayRecord.php @@ -22,10 +22,13 @@ use think\model\relation\BelongsTo; * @property $admin_id 关联玩家所属管理员ID(DicePlayer.admin_id) * @property $lottery_config_id 彩金池配置 * @property $lottery_type 抽奖类型 + * @property $ante 底注/注数(dice_ante_config.mult) + * @property $paid_amount 付费金额(付费局=ante*100,免费局=0) * @property $is_win 是否中大奖:豹子号[1,1,1,1,1]~[6,6,6,6,6]为1,否则0 * @property $win_coin 赢取平台币(= super_win_coin + reward_win_coin) * @property $super_win_coin 中大奖平台币(豹子时发放) * @property $reward_win_coin 摇色子中奖平台币 + * @property $use_coins 消耗平台币(兼容字段:付费局=paid_amount,免费局=0) * @property $direction 方向:0=顺时针,1=逆时针 * @property $reward_config_id 奖励配置id * @property $lottery_id 奖池 diff --git a/server/app/dice/model/player_ticket_record/DicePlayerTicketRecord.php b/server/app/dice/model/player_ticket_record/DicePlayerTicketRecord.php index bcde180..5ba3822 100644 --- a/server/app/dice/model/player_ticket_record/DicePlayerTicketRecord.php +++ b/server/app/dice/model/player_ticket_record/DicePlayerTicketRecord.php @@ -19,6 +19,7 @@ use think\model\relation\BelongsTo; * @property $player_id 玩家id * @property $admin_id 关联玩家所属管理员ID(DicePlayer.admin_id) * @property $use_coins 消耗硬币 + * @property $ante 底注/注数(历史购买记录默认为1;T5再来一次写入本次注数) * @property $total_ticket_count 总抽奖次数 * @property $paid_ticket_count 购买抽奖次数 * @property $free_ticket_count 赠送抽奖次数 diff --git a/server/db/dice_play_record_add_ante_and_paid_amount.sql b/server/db/dice_play_record_add_ante_and_paid_amount.sql new file mode 100644 index 0000000..e4b2e75 --- /dev/null +++ b/server/db/dice_play_record_add_ante_and_paid_amount.sql @@ -0,0 +1,4 @@ +-- DicePlayRecord 新增注数与付费金额字段 +ALTER TABLE `dice_play_record` + ADD COLUMN `ante` int unsigned NOT NULL DEFAULT 1 COMMENT '底注/注数(必须为 dice_ante_config.mult 中存在的值)' AFTER `lottery_type`, + ADD COLUMN `paid_amount` int unsigned NOT NULL DEFAULT 0 COMMENT '付费金额(付费局=ante*100,免费局=0)' AFTER `ante`; diff --git a/server/db/dice_player_ticket_record_add_ante.sql b/server/db/dice_player_ticket_record_add_ante.sql new file mode 100644 index 0000000..66f545d --- /dev/null +++ b/server/db/dice_player_ticket_record_add_ante.sql @@ -0,0 +1,3 @@ +-- DicePlayerTicketRecord 新增注数字段(用于记录“再来一次”免费抽奖的注数) +ALTER TABLE `dice_player_ticket_record` + ADD COLUMN `ante` int unsigned NOT NULL DEFAULT 1 COMMENT '底注/注数(历史购买记录默认为1)' AFTER `use_coins`;