1.优化开奖逻辑

2.优化后台开奖派彩
3.优化接口规范
This commit is contained in:
2026-04-17 13:56:13 +08:00
parent 3cf386756b
commit bf3d50a309
50 changed files with 1036 additions and 770 deletions

View File

@@ -0,0 +1,172 @@
<?php
declare(strict_types=1);
namespace app\common\service;
use support\think\Db;
use Throwable;
/**
* 开奖后结算注单:写入 win_amount、status=已结算;中奖时入账并记 user_wallet_recordbiz_type=payout
*/
final class GameBetSettleService
{
private const BASE_ODDS = 33;
/**
* 对指定期次按开奖号码结算所有「待开奖」注单;同一注单幂等(仅 status=1 会更新)。
*
* @throws Throwable
*/
public static function settleBetsForDraw(int $recordId, int $resultNumber): void
{
if ($recordId <= 0 || $resultNumber < 1) {
return;
}
$now = time();
$bets = Db::name('bet_order')
->where('period_id', $recordId)
->where('status', 1)
->order('id', 'asc')
->select()
->toArray();
foreach ($bets as $bet) {
$betId = (int) ($bet['id'] ?? 0);
if ($betId <= 0) {
continue;
}
$win = self::computeWinAmount($bet, $resultNumber);
$jackpot = '0.0000';
$affected = Db::name('bet_order')
->where('id', $betId)
->where('status', 1)
->update([
'win_amount' => $win,
'jackpot_extra_amount' => $jackpot,
'status' => 2,
'update_time' => $now,
]);
if ($affected === 0) {
continue;
}
if (bccomp($win, '0', 4) <= 0) {
continue;
}
self::creditUserPayout($bet, $betId, $win, $now);
}
}
/**
* 补偿:库中已结束局次但注单仍为待开奖的,可重复调用(幂等)。
*/
public static function settlePendingForEndedRecords(): int
{
$rows = Db::name('game_record')
->where('status', 4)
->whereNotNull('result_number')
->field(['id', 'result_number'])
->order('id', 'asc')
->select()
->toArray();
$count = 0;
foreach ($rows as $row) {
$rid = (int) ($row['id'] ?? 0);
$rn = (int) ($row['result_number'] ?? 0);
if ($rid <= 0 || $rn < 1) {
continue;
}
$pending = Db::name('bet_order')
->where('period_id', $rid)
->where('status', 1)
->count();
if ($pending === 0) {
continue;
}
Db::startTrans();
try {
self::settleBetsForDraw($rid, $rn);
Db::commit();
$count++;
} catch (Throwable $e) {
Db::rollback();
throw $e;
}
}
return $count;
}
/**
* 单注应付派彩:命中开奖号码时 unit × (连胜+1) × 33与 GameLiveService 一致)。
*/
public static function computeWinAmount(array $bet, int $resultNumber): string
{
$pickNumbers = $bet['pick_numbers'] ?? null;
if (is_string($pickNumbers)) {
$decoded = json_decode($pickNumbers, true);
$pickNumbers = is_array($decoded) ? $decoded : [];
}
if (!is_array($pickNumbers)) {
$pickNumbers = [];
}
if (!in_array($resultNumber, array_map('intval', $pickNumbers), true)) {
return '0.0000';
}
$unit = (string) ($bet['unit_amount'] ?? '0');
$streak = (int) ($bet['streak_at_bet'] ?? 0);
$odds = (string) (($streak + 1) * self::BASE_ODDS);
return bcmul($unit, $odds, 4);
}
private static function creditUserPayout(array $bet, int $betId, string $winAmount, int $now): void
{
$userId = (int) ($bet['user_id'] ?? 0);
if ($userId <= 0) {
return;
}
$idem = 'payout_bet_' . $betId;
if (Db::name('user_wallet_record')->where('idempotency_key', $idem)->value('id')) {
return;
}
$user = Db::name('user')->where('id', $userId)->find();
if (!$user) {
return;
}
$before = (string) ($user['coin'] ?? '0');
$after = bcadd($before, $winAmount, 4);
Db::name('user_wallet_record')->insert([
'user_id' => $userId,
'channel_id' => $bet['channel_id'] ?? null,
'biz_type' => 'payout',
'direction' => 1,
'amount' => $winAmount,
'balance_before' => $before,
'balance_after' => $after,
'ref_type' => 'bet_order',
'ref_id' => $betId,
'idempotency_key' => $idem,
'operator_admin_id' => null,
'remark' => '压注派彩',
'create_time' => $now,
]);
Db::name('user')->where('id', $userId)->update([
'coin' => $after,
'update_time' => $now,
]);
}
}

View File

@@ -17,6 +17,9 @@ final class GameLiveService
private const KEY_BET_SECONDS = 'bet_seconds';
private const KEY_PICK_MAX_NUMBER_COUNT = 'pick_max_number_count';
/** 开奖结果号码池1 至此上限(与单注可选号码个数配置无关) */
private const DRAW_NUMBER_MAX = 36;
public static function buildSnapshot(?int $recordId = null): array
{
$record = self::resolveRecord($recordId);
@@ -30,6 +33,7 @@ final class GameLiveService
'period_seconds' => self::getConfigInt(self::KEY_PERIOD_SECONDS, 30),
'bet_seconds' => self::getConfigInt(self::KEY_BET_SECONDS, 20),
'pick_max_number_count' => self::getPickMaxNumberCount(),
'draw_number_max' => self::DRAW_NUMBER_MAX,
'remaining_seconds' => 0,
'bet_remaining_seconds' => 0,
'can_calculate' => false,
@@ -59,7 +63,7 @@ final class GameLiveService
$status = (int) $record['status'];
$canCalculate = $elapsed >= $betSeconds && ($status === 0 || $status === 1);
if ($canCalculate) {
for ($n = 1; $n <= $pickMax; $n++) {
for ($n = 1; $n <= self::DRAW_NUMBER_MAX; $n++) {
$loss = self::estimateLossForNumber($bets, $n);
$candidates[] = [
'number' => $n,
@@ -97,6 +101,7 @@ final class GameLiveService
'period_seconds' => $periodSeconds,
'bet_seconds' => $betSeconds,
'pick_max_number_count' => $pickMax,
'draw_number_max' => self::DRAW_NUMBER_MAX,
'remaining_seconds' => $remaining,
'bet_remaining_seconds' => $betRemaining,
'can_calculate' => $canCalculate,
@@ -129,7 +134,7 @@ final class GameLiveService
}
$pickMax = self::getPickMaxNumberCount();
if ($manualNumber !== null && ($manualNumber < 1 || $manualNumber > $pickMax)) {
if ($manualNumber !== null && ($manualNumber < 1 || $manualNumber > self::DRAW_NUMBER_MAX)) {
return ['ok' => false, 'msg' => '手动开奖号码超出允许范围'];
}
@@ -138,7 +143,7 @@ final class GameLiveService
$bestNumber = null;
$bestLoss = null;
$bestNumbers = [];
for ($n = 1; $n <= $pickMax; $n++) {
for ($n = 1; $n <= self::DRAW_NUMBER_MAX; $n++) {
$loss = self::estimateLossForNumber($bets, $n);
$candidates[] = ['number' => $n, 'estimated_loss' => $loss];
if ($bestLoss === null || bccomp((string) $loss, (string) $bestLoss, 4) < 0) {
@@ -165,6 +170,7 @@ final class GameLiveService
'period_seconds' => $periodSeconds,
'bet_seconds' => $betSeconds,
'pick_max_number_count' => $pickMax,
'draw_number_max' => self::DRAW_NUMBER_MAX,
'candidate_numbers' => $candidates,
'ai_default_number' => $bestNumber,
'final_number' => $finalNumber,
@@ -189,8 +195,10 @@ final class GameLiveService
'draw_mode' => $manualNumber === null ? 0 : 1,
'update_time' => $now,
]);
GameBetSettleService::settleBetsForDraw((int) $record['id'], $finalNumber);
GameRecordService::createNextRecordAfterDraw();
Db::commit();
GameRecordStatService::refreshForRecordId((int) $record['id']);
} catch (Throwable $e) {
Db::rollback();
return ['ok' => false, 'msg' => $e->getMessage()];

View File

@@ -0,0 +1,106 @@
<?php
declare(strict_types=1);
namespace app\common\service;
use support\think\Db;
/**
* 对局维度统计:平台盈亏、中奖人数(与 GameLiveService 单号派彩口径一致)。
*/
final class GameRecordStatService
{
private const BASE_ODDS = 33;
/**
* 根据注单与开奖号码回写 game_record 统计字段(已结束对局)。
*/
public static function refreshForRecordId(int $recordId): void
{
if ($recordId <= 0) {
return;
}
$record = Db::name('game_record')->where('id', $recordId)->find();
if (!$record) {
return;
}
$status = (int) ($record['status'] ?? 0);
$now = time();
if ($status !== 4) {
Db::name('game_record')->where('id', $recordId)->update([
'platform_profit_amount' => '0.0000',
'winner_user_count' => 0,
'update_time' => $now,
]);
return;
}
$resultRaw = $record['result_number'] ?? null;
if ($resultRaw === null || $resultRaw === '') {
return;
}
$resultNum = (int) $resultRaw;
$bets = Db::name('bet_order')->where('period_id', $recordId)->select()->toArray();
$totalBet = '0.0000';
$totalPayout = '0.0000';
$winnerUserIds = [];
foreach ($bets as $bet) {
$st = (int) ($bet['status'] ?? 0);
if ($st === 3) {
continue;
}
$tb = (string) ($bet['total_amount'] ?? '0');
$totalBet = bcadd($totalBet, $tb, 4);
if ($st === 2) {
$payout = bcadd((string) ($bet['win_amount'] ?? '0'), (string) ($bet['jackpot_extra_amount'] ?? '0'), 4);
} else {
$payout = self::estimatePayoutForBet($bet, $resultNum);
}
$totalPayout = bcadd($totalPayout, $payout, 4);
if (bccomp($payout, '0', 4) > 0) {
$uid = (int) ($bet['user_id'] ?? 0);
if ($uid > 0) {
$winnerUserIds[$uid] = true;
}
}
}
$profit = bcsub($totalBet, $totalPayout, 4);
Db::name('game_record')->where('id', $recordId)->update([
'platform_profit_amount' => $profit,
'winner_user_count' => count($winnerUserIds),
'update_time' => $now,
]);
}
/**
* 与 GameLiveService::estimateLossForNumber 中单注派彩一致:命中号码时 unit × (streak+1) × 33。
*/
private static function estimatePayoutForBet(array $bet, int $resultNumber): string
{
$pickNumbers = $bet['pick_numbers'] ?? null;
if (is_string($pickNumbers)) {
$decoded = json_decode($pickNumbers, true);
$pickNumbers = is_array($decoded) ? $decoded : [];
}
if (!is_array($pickNumbers)) {
$pickNumbers = [];
}
if (!in_array($resultNumber, array_map('intval', $pickNumbers), true)) {
return '0.0000';
}
$unit = (string) ($bet['unit_amount'] ?? '0');
$streak = (int) ($bet['streak_at_bet'] ?? 0);
$odds = (string) (($streak + 1) * self::BASE_ODDS);
return bcmul($unit, $odds, 4);
}
}