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

[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

@@ -8,6 +8,8 @@
"direction": "Direction",
"isBigWin": "Is Big Win",
"winCoin": "Win Coin",
"paidAmount": "Paid Amount",
"ante": "Ante",
"rewardTier": "Reward Tier",
"rollNumber": "Roll Number",
"paid": "Paid",
@@ -23,6 +25,8 @@
"lotteryPoolConfig": "Lottery Pool Config",
"drawType": "Draw Type",
"isBigWin": "Is Big Win",
"paidAmount": "Paid Amount",
"ante": "Ante",
"winCoin": "Win Coin",
"superWinCoin": "Super Win Coin",
"rewardWinCoin": "Reward Win Coin",

View File

@@ -19,6 +19,7 @@
"search": {
"player": "Player",
"useCoins": "Use Coins",
"ante": "Ante",
"totalDrawCount": "Total Draw Count",
"paidDrawCount": "Paid Draw Count",
"freeDrawCount": "Free Draw Count",
@@ -29,6 +30,7 @@
"id": "ID",
"playerUsername": "Player Username",
"useCoins": "Use Coins",
"ante": "Ante",
"totalDrawCount": "Total Draw Count",
"paidDrawCount": "Paid Draw Count",
"freeDrawCount": "Free Draw Count",

View File

@@ -61,6 +61,7 @@
"stepFree": "Free ticket",
"labelLotteryTypePaid": "Test pool type",
"labelLotteryTypeFree": "Test pool type",
"labelAnte": "Ante",
"placeholderPaidPool": "Leave empty for custom tier odds below (default: default)",
"placeholderFreePool": "Leave empty for custom tier odds below (default: killScore)",
"tierProbHint": "Custom tier odds (T1T5), each 0100%, sum of five must not exceed 100%",
@@ -73,6 +74,7 @@
"btnNext": "Next",
"btnStart": "Start test",
"btnCancel": "Cancel",
"warnAnte": "Ante must be greater than 0",
"warnTotalSpins": "At least one of paid/free direction spin counts must be greater than 0",
"warnPaidTierSumPositive": "When no paid pool is selected, T1T5 odds sum must be greater than 0",
"warnPaidTierSumMax": "Paid T1T5 odds sum cannot exceed 100%",

View File

@@ -12,6 +12,7 @@
"platformProfit": "Platform Profit",
"totalDrawCount": "Total Draw Count",
"createdBy": "Created By",
"remark": "Remark",
"createTime": "Create Time",
"statusFail": "Failed",
"statusDone": "Done",

View File

@@ -8,7 +8,7 @@
"placeholderLotteryPool": "请选择彩金池配置",
"drawType": "抽奖类型",
"paid": "付费",
"free": "赠送",
"free": "免费",
"isBigWin": "是否中大奖",
"noBigWin": "无",
"bigWin": "中大奖",
@@ -53,7 +53,7 @@
"nameFuzzy": "名称模糊",
"uiTextFuzzy": "前端显示文本模糊",
"paid": "付费",
"free": "赠送",
"free": "免费",
"noBigWin": "无",
"bigWin": "中大奖",
"clockwise": "顺时针",

View File

@@ -8,10 +8,12 @@
"direction": "方向",
"isBigWin": "是否中大奖",
"winCoin": "赢取平台币",
"paidAmount": "付费金额",
"ante": "底注",
"rewardTier": "奖励档位",
"rollNumber": "摇取点数和",
"paid": "付费",
"free": "赠送",
"free": "免费",
"clockwise": "顺时针",
"anticlockwise": "逆时针",
"noBigWin": "无",
@@ -23,6 +25,8 @@
"lotteryPoolConfig": "彩金池配置",
"drawType": "抽奖类型",
"isBigWin": "是否中大奖",
"paidAmount": "付费金额",
"ante": "底注",
"winCoin": "赢取平台币",
"superWinCoin": "中大奖平台币",
"rewardWinCoin": "摇色子中奖平台币",

View File

@@ -19,6 +19,7 @@
"search": {
"player": "玩家",
"useCoins": "消耗硬币",
"ante": "底注",
"totalDrawCount": "总抽奖次数",
"paidDrawCount": "购买抽奖次数",
"freeDrawCount": "赠送抽奖次数",
@@ -29,6 +30,7 @@
"id": "ID",
"playerUsername": "玩家用户名",
"useCoins": "消耗硬币",
"ante": "底注",
"totalDrawCount": "总抽奖次数",
"paidDrawCount": "购买抽奖次数",
"freeDrawCount": "赠送抽奖次数",

View File

@@ -61,6 +61,7 @@
"stepFree": "免费抽奖券",
"labelLotteryTypePaid": "测试数据档位类型",
"labelLotteryTypeFree": "测试数据档位类型",
"labelAnte": "底注 ante",
"placeholderPaidPool": "不选则下方自定义档位概率(默认 default",
"placeholderFreePool": "不选则下方自定义档位概率(默认 killScore",
"tierProbHint": "自定义档位概率T1T5每档 0-100%,五档之和不能超过 100%",
@@ -73,6 +74,7 @@
"btnNext": "下一步",
"btnStart": "开始测试",
"btnCancel": "取消",
"warnAnte": "底注 ante 必须大于 0",
"warnTotalSpins": "付费或免费至少一种方向次数之和大于 0",
"warnPaidTierSumPositive": "付费未选奖池时T1T5 档位概率之和需大于 0",
"warnPaidTierSumMax": "付费档位概率 T1T5 之和不能超过 100%",

View File

@@ -12,6 +12,7 @@
"platformProfit": "平台赚取金额",
"totalDrawCount": "总抽奖次数",
"createdBy": "创建管理员",
"remark": "备注",
"createTime": "创建时间",
"statusFail": "失败",
"statusDone": "完成",

View File

@@ -61,6 +61,7 @@ export default {
* 可选 lottery_config_id不选则传 paid_tier_weights / free_tier_weightsT1-T5
*/
startWeightTest(params: {
ante?: number
lottery_config_id?: number
paid_lottery_config_id?: number
free_lottery_config_id?: number

View File

@@ -58,19 +58,37 @@
<!-- 抽奖类型 -->
<template #lottery_type="{ row }">
<ElTag size="small" :type="row.lottery_type === 0 ? 'warning' : 'success'">
{{ row.lottery_type === 0 ? t('page.search.paid') : row.lottery_type === 1 ? t('page.search.free') : '-' }}
{{
row.lottery_type === 0
? t('page.search.paid')
: row.lottery_type === 1
? t('page.search.free')
: '-'
}}
</ElTag>
</template>
<!-- 是否中大奖 -->
<template #is_win="{ row }">
<ElTag size="small" :type="row.is_win === 1 ? 'success' : 'info'">
{{ row.is_win === 0 ? t('page.search.noBigWin') : row.is_win === 1 ? t('page.search.bigWin') : '-' }}
{{
row.is_win === 0
? t('page.search.noBigWin')
: row.is_win === 1
? t('page.search.bigWin')
: '-'
}}
</ElTag>
</template>
<!-- 方向 -->
<template #direction="{ row }">
<ElTag size="small" :type="row.direction === 0 ? 'primary' : 'warning'">
{{ row.direction === 0 ? t('page.search.clockwise') : row.direction === 1 ? t('page.search.anticlockwise') : '-' }}
{{
row.direction === 0
? t('page.search.clockwise')
: row.direction === 1
? t('page.search.anticlockwise')
: '-'
}}
</ElTag>
</template>
<!-- 摇取点数 -->
@@ -132,6 +150,8 @@
lottery_type: undefined,
direction: undefined,
is_win: undefined,
paid_amount: undefined,
ante: undefined,
win_coin_min: undefined,
win_coin_max: undefined,
reward_tier: undefined,
@@ -208,9 +228,16 @@
columnsFactory: () => [
{ type: 'selection' },
{ prop: 'id', label: 'page.table.id', width: 80 },
{ prop: 'lottery_config_id', label: 'page.table.lotteryPoolConfig', width: 120, useSlot: true },
{
prop: 'lottery_config_id',
label: 'page.table.lotteryPoolConfig',
width: 120,
useSlot: true
},
{ prop: 'lottery_type', label: 'page.table.drawType', width: 100, useSlot: true },
{ prop: 'is_win', label: 'page.table.isBigWin', width: 100, useSlot: true },
{ prop: 'paid_amount', label: 'page.table.paidAmount', width: 130 },
{ prop: 'ante', label: 'page.table.ante', width: 90 },
{ prop: 'win_coin', label: 'page.table.winCoin', width: 110 },
{ prop: 'super_win_coin', label: 'page.table.superWinCoin', width: 120 },
{ prop: 'reward_win_coin', label: 'page.table.rewardWinCoin', width: 140 },
@@ -222,7 +249,13 @@
{ prop: 'reward_config_id', label: 'page.table.rewardConfig', width: 100, useSlot: true },
{ prop: 'status', label: 'page.table.status', width: 80, useSlot: true },
{ prop: 'create_time', label: 'page.table.createTime', width: 170 },
{ prop: 'operation', label: 'table.actions.operation', width: 100, fixed: 'right', useSlot: true }
{
prop: 'operation',
label: 'table.actions.operation',
width: 100,
fixed: 'right',
useSlot: true
}
]
}
})

View File

@@ -28,6 +28,24 @@
<el-option :label="$t('page.search.anticlockwise')" :value="1" />
</el-select>
</el-form-item>
<el-form-item :label="$t('page.search.ante')" prop="ante">
<el-input-number
v-model="formData.ante"
:placeholder="$t('table.searchBar.all')"
:precision="0"
controls-position="right"
style="width: 100%"
/>
</el-form-item>
<el-form-item :label="$t('page.search.paidAmount')" prop="paid_amount">
<el-input-number
v-model="formData.paid_amount"
:placeholder="$t('table.searchBar.all')"
:precision="0"
controls-position="right"
style="width: 100%"
/>
</el-form-item>
<el-form-item :label="$t('page.search.isBigWin')" prop="is_win">
<el-select v-model="formData.is_win" :placeholder="$t('form.placeholderSelect')" clearable style="width: 100%">
<el-option :label="$t('page.search.noBigWin')" :value="0" />
@@ -153,6 +171,8 @@
lottery_config_id: null,
lottery_type: null,
is_win: null,
ante: 1,
paid_amount: 0,
win_coin: 0,
direction: null,
reward_tier: undefined as string | undefined,

View File

@@ -32,6 +32,28 @@
</el-select>
</el-form-item>
</el-col>
<el-col v-bind="setSpan(6)">
<el-form-item :label="$t('page.search.paidAmount')" prop="paid_amount">
<el-input-number
v-model="formData.paid_amount"
:placeholder="$t('table.searchBar.all')"
:precision="0"
controls-position="right"
style="width: 100%"
/>
</el-form-item>
</el-col>
<el-col v-bind="setSpan(6)">
<el-form-item :label="$t('page.search.ante')" prop="ante">
<el-input-number
v-model="formData.ante"
:placeholder="$t('table.searchBar.all')"
:precision="0"
controls-position="right"
style="width: 100%"
/>
</el-form-item>
</el-col>
<el-col v-bind="setSpan(6)">
<el-form-item :label="$t('page.search.winCoin')" prop="win_coin_min">
<div class="range-wrap">

View File

@@ -82,6 +82,7 @@
username: undefined,
use_coins_min: undefined,
use_coins_max: undefined,
ante: undefined,
total_ticket_count_min: undefined,
total_ticket_count_max: undefined,
paid_ticket_count_min: undefined,
@@ -136,6 +137,7 @@
formatter: (row: Record<string, any>) => usernameFormatter(row)
},
{ prop: 'use_coins', label: 'page.table.useCoins', align: 'center' },
{ prop: 'ante', label: 'page.table.ante', align: 'center' },
{ prop: 'total_ticket_count', label: 'page.table.totalDrawCount', align: 'center' },
{ prop: 'paid_ticket_count', label: 'page.table.paidDrawCount', align: 'center' },
{ prop: 'free_ticket_count', label: 'page.table.freeDrawCount', align: 'center' },

View File

@@ -34,6 +34,18 @@
</div>
</el-form-item>
</el-col>
<el-col v-bind="setSpan(6)">
<el-form-item :label="$t('page.search.ante')" prop="ante">
<el-input-number
v-model="formData.ante"
:placeholder="$t('table.searchBar.all')"
:min="1"
:precision="0"
controls-position="right"
style="width: 100%"
/>
</el-form-item>
</el-col>
<el-col v-bind="setSpan(6)">
<el-form-item :label="$t('page.search.totalDrawCount')" prop="total_ticket_count_min">
<div class="range-wrap">

View File

@@ -12,6 +12,9 @@
{{ $t('page.weightTest.alertBody') }}
</ElAlert>
<ElForm ref="formRef" :model="form" label-width="140px">
<ElFormItem :label="$t('page.weightTest.labelAnte')" prop="ante" required>
<ElInputNumber v-model="form.ante" :min="1" :step="1" style="width: 100%" />
</ElFormItem>
<ElSteps :active="currentStep" finish-status="success" simple class="steps-wrap">
<ElStep :title="$t('page.weightTest.stepPaid')" />
<ElStep :title="$t('page.weightTest.stepFree')" />
@@ -187,6 +190,7 @@
const formRef = ref()
const currentStep = ref(0)
const form = reactive({
ante: 1,
paid_lottery_config_id: undefined as number | undefined,
free_lottery_config_id: undefined as number | undefined,
paid_tier_weights: { T1: 20, T2: 20, T3: 20, T4: 20, T5: 20 } as Record<string, number>,
@@ -270,6 +274,7 @@
function buildPayload() {
const payload: Record<string, unknown> = {
ante: form.ante,
paid_s_count: form.paid_s_count,
paid_n_count: form.paid_n_count,
free_s_count: form.free_s_count,
@@ -289,6 +294,10 @@
}
function validateForm(): boolean {
if (form.ante == null || form.ante <= 0) {
ElMessage.warning(t('page.weightTest.warnAnte'))
return false
}
if (form.paid_s_count + form.paid_n_count + form.free_s_count + form.free_n_count <= 0) {
ElMessage.warning(t('page.weightTest.warnTotalSpins'))
return false

View File

@@ -214,6 +214,13 @@
align: 'center',
showOverflowTooltip: true
},
{
prop: 'remark',
label: 'page.table.remark',
width: 220,
align: 'center',
showOverflowTooltip: true
},
{ prop: 'create_time', label: 'page.table.createTime', width: 170, align: 'center' },
{
prop: 'operation',

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必须填写',