[色子游戏]抽奖记录(测试权重)-优化

[API]记录抽奖券DicePlayerTicketRecord
This commit is contained in:
2026-03-25 18:51:29 +08:00
parent f8cf85dd01
commit d793a511ee
27 changed files with 256 additions and 47 deletions

View File

@@ -284,6 +284,21 @@ class PlayStartLogic
$p->free_ticket_count = max(0, (int) $p->free_ticket_count - 1);
}
// 记录每次游玩:写入抽奖券记录(用于后台“抽奖券记录”追踪付费/免费游玩与消耗)
$isPaidPlay = $ticketType === self::LOTTERY_TYPE_PAID;
$paidCnt = $isPaidPlay ? 1 : 0;
$freeCnt = $isPaidPlay ? 0 : 1;
DicePlayerTicketRecord::create([
'player_id' => $playerId,
'admin_id' => $adminId,
'use_coins' => $paidAmount,
'ante' => $ante,
'total_ticket_count' => $paidCnt + $freeCnt,
'paid_ticket_count' => $paidCnt,
'free_ticket_count' => $freeCnt,
'remark' => ($isPaidPlay ? '付费游玩' : '免费游玩') . '|play_record_id=' . $record->id,
]);
// 若本局中奖档位为 T5则额外赠送 1 次免费抽奖次数(总次数也 +1并记录抽奖券获取记录
if ($isTierT5) {
$p->free_ticket_count = (int) $p->free_ticket_count + 1;
@@ -509,10 +524,11 @@ class PlayStartLogic
* @param \app\dice\model\lottery_pool_config\DiceLotteryPoolConfig|null $config 奖池配置,自定义档位时可为 null
* @param int $direction 0=顺时针 1=逆时针
* @param int $lotteryType 0=付费 1=免费
* @param int $ante 底注/注数dice_ante_config.mult
* @param array|null $customTierWeights 自定义档位权重 ['T1'=>x, 'T2'=>x, ...],非空时忽略 config 的档位权重
* @return array 可直接用于 DicePlayRecordTest::create 的字段 + tier用于统计档位概率
*/
public function simulateOnePlay($config, int $direction, int $lotteryType = 0, ?array $customTierWeights = null): array
public function simulateOnePlay($config, int $direction, int $lotteryType = 0, int $ante = 1, ?array $customTierWeights = null): array
{
$rewardInstance = DiceReward::getCachedInstance();
$byTierDirection = $rewardInstance['by_tier_direction'] ?? [];
@@ -558,8 +574,8 @@ class PlayStartLogic
$targetIndex = (int) ($chosen['end_index'] ?? 0);
$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;
$superWinCoin = 0;
$isWin = 0;
@@ -588,8 +604,11 @@ 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;
$rewardWinCoin = 0;
// 中豹子时不走原奖励流程
$realEv = 0.0;
} else {
$rollArray = $this->generateNonSuperWinRollArrayWithSum($rollNumber);
}
@@ -603,6 +622,7 @@ class PlayStartLogic
$rewardId = ($isWin === 1 && $superWinCoin > 0) ? 0 : $targetIndex;
$configName = $config !== null ? (string) ($config->name ?? '') : '自定义';
$costRealEv = $realEv + ($isWin === 1 ? $bigWinRealEv : 0.0);
$paidAmount = $lotteryType === 0 ? ($ante * self::UNIT_COST) : 0;
return [
'player_id' => 0,
@@ -611,9 +631,11 @@ class PlayStartLogic
'lottery_type' => $lotteryType,
'is_win' => $isWin,
'win_coin' => $winCoin,
'ante' => $ante,
'paid_amount' => $paidAmount,
'super_win_coin' => $superWinCoin,
'reward_win_coin' => $rewardWinCoin,
'use_coins' => 0,
'use_coins' => $paidAmount,
'direction' => $direction,
'reward_config_id' => $rewardId,
'start_index' => $startIndex,

View File

@@ -43,18 +43,20 @@ class DicePlayRecordTestController extends BaseController
['is_win', ''],
['win_coin_min', ''],
['win_coin_max', ''],
['paid_amount', ''],
['ante', ''],
['reward_tier', ''],
['roll_number', ''],
]);
$query = $this->logic->search($where);
$query->with(['diceLotteryPoolConfig', 'diceRewardConfig']);
// 按当前筛选条件统计:平台总盈利 = 付费抽奖(lottery_type=0)次数×100 - 玩家总收益(win_coin 求和)
// 按当前筛选条件统计:平台总盈利 = 付费金额(paid_amount 求和) - 玩家总收益(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;

View File

@@ -42,6 +42,7 @@ class DicePlayerTicketRecordController extends BaseController
['username', ''],
['use_coins_min', ''],
['use_coins_max', ''],
['ante', ''],
['total_ticket_count_min', ''],
['total_ticket_count_max', ''],
['paid_ticket_count_min', ''],

View File

@@ -90,6 +90,7 @@ class DiceRewardController extends BaseController
{
$post = is_array($request->post()) ? $request->post() : [];
$params = [
'ante' => $post['ante'] ?? null,
'lottery_config_id' => $post['lottery_config_id'] ?? null,
'paid_lottery_config_id' => $post['paid_lottery_config_id'] ?? null,
'free_lottery_config_id' => $post['free_lottery_config_id'] ?? null,

View File

@@ -5,6 +5,7 @@
namespace app\dice\logic\reward_config_record;
use app\dice\model\lottery_pool_config\DiceLotteryPoolConfig;
use app\dice\model\ante_config\DiceAnteConfig;
use app\dice\model\reward\DiceReward;
use app\dice\model\reward\DiceRewardConfig;
use app\dice\model\reward_config_record\DiceRewardConfigRecord;
@@ -250,6 +251,15 @@ class DiceRewardConfigRecordLogic extends BaseLogic
$adminId = $adminIdOrFreeS !== null && $adminIdOrFreeS !== '' ? (int) $adminIdOrFreeS : null;
}
$allowed = [100, 500, 1000, 5000];
$ante = isset($params['ante']) ? intval($params['ante']) : 1;
if ($ante <= 0) {
throw new ApiException('ante must be greater than 0');
}
$anteExists = DiceAnteConfig::where('mult', $ante)->count();
if ($anteExists <= 0) {
throw new ApiException('ante not allowed: ' . $ante);
}
$lotteryConfigId = isset($params['lottery_config_id']) ? (int) $params['lottery_config_id'] : 0;
$paidConfigId = isset($params['paid_lottery_config_id']) ? (int) $params['paid_lottery_config_id'] : 0;
$freeConfigId = isset($params['free_lottery_config_id']) ? (int) $params['free_lottery_config_id'] : 0;
@@ -407,6 +417,7 @@ class DiceRewardConfigRecordLogic extends BaseLogic
$record->result_counts = [];
$record->tier_counts = null;
$record->bigwin_weight = $bigwinWeights ?: null;
$record->ante = $ante;
$record->admin_id = $adminId;
$record->create_time = date('Y-m-d H:i:s');
$record->save();

View File

@@ -33,6 +33,7 @@ class WeightTestRunner
return;
}
$ante = is_numeric($record->ante ?? null) ? intval($record->ante) : 1;
$paidS = (int) ($record->paid_s_count ?? 0);
$paidN = (int) ($record->paid_n_count ?? 0);
$freeS = (int) ($record->free_s_count ?? 0);
@@ -60,28 +61,40 @@ class WeightTestRunner
$safetyLine = (int) ($configType0->safety_line ?? 0);
$killEnabled = ((int) ($configType0->kill_enabled ?? 1)) === 1;
$paidTierWeights = (is_array($record->paid_tier_weights ?? null) && $record->paid_tier_weights !== [])
$paidTierWeightsCustom = (is_array($record->paid_tier_weights ?? null) && $record->paid_tier_weights !== [])
? $record->paid_tier_weights
: [
'T1' => (int) ($configType0->t1_weight ?? 0),
'T2' => (int) ($configType0->t2_weight ?? 0),
'T3' => (int) ($configType0->t3_weight ?? 0),
'T4' => (int) ($configType0->t4_weight ?? 0),
'T5' => (int) ($configType0->t5_weight ?? 0),
];
if (array_sum($paidTierWeights) <= 0) {
$this->markFailed($recordId, '需提供 paid_tier_weights玩家权重盈利未达安全线时付费抽奖使用或选择 default 奖池');
return;
: null;
$freeTierWeightsCustom = (is_array($record->free_tier_weights ?? null) && $record->free_tier_weights !== [])
? $record->free_tier_weights
: null;
$paidPoolConfigId = (int) ($record->paid_lottery_config_id ?? 0);
$freePoolConfigId = (int) ($record->free_lottery_config_id ?? 0);
$paidPoolConfig = $paidPoolConfigId > 0 ? DiceLotteryPoolConfig::find($paidPoolConfigId) : $configType0;
if (!$paidPoolConfig) {
$paidPoolConfig = $configType0;
}
$freePoolConfig = $freePoolConfigId > 0 ? DiceLotteryPoolConfig::find($freePoolConfigId) : $configType1;
if (!$freePoolConfig) {
$freePoolConfig = $configType0;
}
$freeConfig = $configType1 !== null ? $configType1 : $configType0;
if ($paidTierWeightsCustom !== null && array_sum($paidTierWeightsCustom) <= 0) {
$this->markFailed($recordId, 'paid_tier_weights玩家权重之和必须大于 0');
return;
}
if ($freeTierWeightsCustom !== null && array_sum($freeTierWeightsCustom) <= 0) {
$this->markFailed($recordId, 'free_tier_weights免费玩家权重之和必须大于 0');
return;
}
// 每次测试开始前清空进程内静态缓存,强制从共享缓存读取最新 BIGWIN/奖励配置,与数据库一致
DiceRewardConfig::clearRequestInstance();
DiceReward::clearRequestInstance();
// 彩金池累计盈利:用于判断是否触发杀分(不再依赖单个玩家累计盈利)
$poolProfitTotal = $configType0->profit_amount ?? 0;
$poolProfitTotal = floatval($configType0->profit_amount ?? 0);
$playLogic = new PlayStartLogic();
$resultCounts = [];
@@ -92,9 +105,9 @@ class WeightTestRunner
try {
for ($i = 0; $i < $paidS; $i++) {
$usePoolWeights = $killEnabled && $poolProfitTotal >= $safetyLine && $configType1 !== null;
$paidConfig = $usePoolWeights ? $configType1 : $configType0;
$customWeights = $usePoolWeights ? null : $paidTierWeights;
$row = $playLogic->simulateOnePlay($paidConfig, 0, 0, $customWeights);
$paidConfig = $usePoolWeights ? $configType1 : $paidPoolConfig;
$customWeights = $usePoolWeights ? null : $paidTierWeightsCustom;
$row = $playLogic->simulateOnePlay($paidConfig, 0, 0, $ante, $customWeights);
$this->accumulateProfitForDefault($row, 0, $paidConfig, $configType0, $poolProfitTotal);
$this->aggregate($row, $resultCounts, $tierCounts);
$buffer[] = $this->rowForInsert($row, $recordId);
@@ -103,9 +116,9 @@ class WeightTestRunner
}
for ($i = 0; $i < $paidN; $i++) {
$usePoolWeights = $killEnabled && $poolProfitTotal >= $safetyLine && $configType1 !== null;
$paidConfig = $usePoolWeights ? $configType1 : $configType0;
$customWeights = $usePoolWeights ? null : $paidTierWeights;
$row = $playLogic->simulateOnePlay($paidConfig, 1, 0, $customWeights);
$paidConfig = $usePoolWeights ? $configType1 : $paidPoolConfig;
$customWeights = $usePoolWeights ? null : $paidTierWeightsCustom;
$row = $playLogic->simulateOnePlay($paidConfig, 1, 0, $ante, $customWeights);
$this->accumulateProfitForDefault($row, 0, $paidConfig, $configType0, $poolProfitTotal);
$this->aggregate($row, $resultCounts, $tierCounts);
$buffer[] = $this->rowForInsert($row, $recordId);
@@ -113,7 +126,10 @@ class WeightTestRunner
$this->flushIfNeeded($buffer, $recordId, $done, $total, $resultCounts, $tierCounts);
}
for ($i = 0; $i < $freeS; $i++) {
$row = $playLogic->simulateOnePlay($freeConfig, 0, 1, null);
$useKillMode = $killEnabled && $poolProfitTotal >= $safetyLine && $configType1 !== null;
$freeConfig = $useKillMode ? $configType1 : $freePoolConfig;
$customWeights = $useKillMode ? null : $freeTierWeightsCustom;
$row = $playLogic->simulateOnePlay($freeConfig, 0, 1, $ante, $customWeights);
$this->accumulateProfitForDefault($row, 1, $freeConfig, $configType0, $poolProfitTotal);
$this->aggregate($row, $resultCounts, $tierCounts);
$buffer[] = $this->rowForInsert($row, $recordId);
@@ -121,7 +137,10 @@ class WeightTestRunner
$this->flushIfNeeded($buffer, $recordId, $done, $total, $resultCounts, $tierCounts);
}
for ($i = 0; $i < $freeN; $i++) {
$row = $playLogic->simulateOnePlay($freeConfig, 1, 1, null);
$useKillMode = $killEnabled && $poolProfitTotal >= $safetyLine && $configType1 !== null;
$freeConfig = $useKillMode ? $configType1 : $freePoolConfig;
$customWeights = $useKillMode ? null : $freeTierWeightsCustom;
$row = $playLogic->simulateOnePlay($freeConfig, 1, 1, $ante, $customWeights);
$this->accumulateProfitForDefault($row, 1, $freeConfig, $configType0, $poolProfitTotal);
$this->aggregate($row, $resultCounts, $tierCounts);
$buffer[] = $this->rowForInsert($row, $recordId);
@@ -153,7 +172,8 @@ class WeightTestRunner
return;
}
$winCoin = (float) $row['win_coin'];
$playerProfitTotal += $lotteryType === 0 ? ($winCoin - 100.0) : $winCoin;
$paidAmount = (float) ($row['paid_amount'] ?? 0);
$playerProfitTotal += $lotteryType === 0 ? ($winCoin - $paidAmount) : $winCoin;
}
private function aggregate(array $row, array &$resultCounts, array &$tierCounts): void
@@ -176,6 +196,7 @@ class WeightTestRunner
$keys = [
'player_id', 'admin_id', 'lottery_config_id', 'lottery_type', 'is_win', 'win_coin',
'super_win_coin', 'reward_win_coin', 'use_coins', 'direction', 'reward_config_id',
'ante', 'paid_amount',
'start_index', 'target_index', 'roll_array', 'roll_number', 'lottery_name', 'status',
];
foreach ($keys as $k) {
@@ -219,7 +240,7 @@ class WeightTestRunner
/**
* 标记测试成功并记录平台总盈利 platform_profit
* 通过关联 DicePlayRecordTestreward_config_record_id统计付费(lottery_type=0)次数×100 - win_coin 求和
* 通过关联 DicePlayRecordTestreward_config_record_id统计付费金额 paid_amount 求和 - win_coin 求和
*/
private function markSuccess(int $recordId, array $resultCounts, array $tierCounts): void
{

View File

@@ -19,9 +19,11 @@ use think\model\relation\BelongsTo;
*
* @property $id ID
* @property $lottery_config_id 彩金池配置id
* @property $lottery_type 抽奖类型:0=付费,1=赠送
* @property $lottery_type 抽奖类型:0=付费,1=免费
* @property $is_win 中大奖:0=无,1=中奖
* @property $win_coin 赢取平台币
* @property int|null $ante 底注/注数dice_ante_config.mult
* @property int|null $paid_amount 付费金额(付费局=ante*100免费局=0
* @property $direction 方向:0=顺时针,1=逆时针
* @property $reward_config_id 奖励配置id
* @property $create_time 创建时间
@@ -77,7 +79,7 @@ class DicePlayRecordTest extends BaseModel
return $this->belongsTo(DiceRewardConfigRecord::class, 'reward_config_record_id', 'id');
}
/** 抽奖类型 0=付费 1=赠送 */
/** 抽奖类型 0=付费 1=免费 */
public function searchLotteryTypeAttr($query, $value)
{
if ($value !== '' && $value !== null) {
@@ -117,6 +119,22 @@ class DicePlayRecordTest extends BaseModel
}
}
/** 付费金额(付费局=ante*100免费局=0 */
public function searchPaidAmountAttr($query, $value)
{
if ($value !== '' && $value !== null) {
$query->where('paid_amount', '=', $value);
}
}
/** 底注/注数dice_ante_config.mult */
public function searchAnteAttr($query, $value)
{
if ($value !== '' && $value !== null) {
$query->where('ante', '=', $value);
}
}
/** 中奖档位(按 reward_config_id 对应 DiceRewardConfig.tier */
public function searchRewardTierAttr($query, $value)
{

View File

@@ -144,4 +144,12 @@ class DicePlayerTicketRecord extends BaseModel
$query->where('create_time', '<=', $value);
}
}
/** 底注/注数ante */
public function searchAnteAttr($query, $value)
{
if ($value !== '' && $value !== null) {
$query->where('ante', '=', $value);
}
}
}

View File

@@ -26,6 +26,7 @@ use think\model\relation\HasMany;
* @property int $over_play_count 已完成次数
* @property int $status 状态 -1失败 0进行中 1成功
* @property string|null $remark 失败时记录原因
* @property int|null $ante 底注/注数dice_ante_config.mult
* @property int $s_count 顺时针模拟次数(兼容旧数据)
* @property int $n_count 逆时针模拟次数(兼容旧数据)
* @property int $paid_s_count 付费抽奖顺时针次数
@@ -70,18 +71,18 @@ class DiceRewardConfigRecord extends BaseModel
/**
* 根据关联的 DicePlayRecordTest 统计平台赚取平台币
* platform_profit = 关联的付费(lottery_type=0)抽取次数 × 100 - 关联的 win_coin 求和
* platform_profit = 关联的付费(lottery_type=0)付费金额求和(paid_amount) - 关联的 win_coin 求和
* @param int $recordId dice_reward_config_record.id
* @return float
*/
public static function computePlatformProfitFromRelated(int $recordId): float
{
$paidCount = DicePlayRecordTest::where('reward_config_record_id', $recordId)
$paidAmount = (float) DicePlayRecordTest::where('reward_config_record_id', $recordId)
->where('lottery_type', 0)
->count();
->sum('paid_amount');
$sumWinCoin = (float) DicePlayRecordTest::where('reward_config_record_id', $recordId)
->sum('win_coin');
return round($paidCount * 100 - $sumWinCoin, 2);
return round($paidAmount - $sumWinCoin, 2);
}
/**

View File

@@ -30,7 +30,7 @@ class DicePlayRecordTestValidate extends BaseValidate
*/
protected $message = [
'lottery_config_id' => '彩金池配置id必须填写',
'lottery_type' => '抽奖类型:0=付费,1=赠送必须填写',
'lottery_type' => '抽奖类型:0=付费,1=免费必须填写',
'is_win' => '中大奖:0=无,1=中奖必须填写',
'direction' => '方向:0=顺时针,1=逆时针必须填写',
'reward_config_id' => '奖励配置id必须填写',