diff --git a/saiadmin-artd/src/views/plugin/dice/reward_config_record/index/modules/detail-drawer.vue b/saiadmin-artd/src/views/plugin/dice/reward_config_record/index/modules/detail-drawer.vue index 56c04cd..ce8920e 100644 --- a/saiadmin-artd/src/views/plugin/dice/reward_config_record/index/modules/detail-drawer.vue +++ b/saiadmin-artd/src/views/plugin/dice/reward_config_record/index/modules/detail-drawer.vue @@ -27,6 +27,14 @@ {{ record.free_lottery_config_id ?? '—' }} + + + + @@ -68,19 +76,53 @@
权重配比快照(测试时使用的 T1-T5/BIGWIN 配置)
- - - - - - -
暂无快照数据
+ +
+
顺时针(非 BIGWIN)
+ + + + + +
暂无顺时针数据
+
+ +
+
逆时针(非 BIGWIN)
+ + + + + +
暂无逆时针数据
+
+ +
+
BIGWIN(按 DiceRewardConfig 配置快照)
+ + + + +
暂无 BIGWIN 数据
+
@@ -181,6 +223,7 @@ lottery_config_id?: number | null paid_lottery_config_id?: number | null free_lottery_config_id?: number | null + bigwin_weight?: Record | Array<[number, number]> | null // 新结构:{ paid: {T1..T5}, free: {T1..T5} },兼容旧结构直接是 {T1..T5} tier_weights_snapshot?: | { @@ -279,6 +322,26 @@ return tierWeightsToTableData(source || undefined) }) + const bigwinWeightDisplay = computed(() => { + const raw = props.record?.bigwin_weight + if (!raw) return [] + const entries: Array<{ grid: number; weight: number }> = [] + if (Array.isArray(raw)) { + raw.forEach(([grid, weight]) => { + entries.push({ grid: Number(grid), weight: Number(weight) }) + }) + } else if (typeof raw === 'object') { + Object.keys(raw).forEach((k) => { + const grid = Number(k) + const w = Number((raw as Record)[k]) + if (!Number.isNaN(grid) && !Number.isNaN(w)) { + entries.push({ grid, weight: w }) + } + }) + } + return entries.sort((a, b) => a.grid - b.grid) + }) + // 导入不限制奖池类型,两个下拉都可选任意 DiceLotteryPoolConfig const paidLotteryOptions = computed(() => lotteryConfigOptions.value) const freeLotteryOptions = computed(() => lotteryConfigOptions.value) @@ -289,25 +352,65 @@ }) const snapshotTableData = computed(() => { - const snapshot = props.record?.weight_config_snapshot + const snapshot = props.record?.weight_config_snapshot as + | Array<{ + tier?: string + direction?: number + grid_number?: number + weight?: number + }> + | undefined if (!Array.isArray(snapshot)) return [] - return snapshot.map((item) => ({ - id: item.id ?? '—', - grid_number: item.grid_number ?? '—', - tier: item.tier ?? '—', - weight: item.weight ?? '—' - })) + return snapshot.map((item) => { + const dir = item.direction + return { + tier: item.tier ?? '—', + direction: dir, + direction_label: dir === 0 ? '顺时针' : dir === 1 ? '逆时针' : '—', + grid_number: item.grid_number ?? '—', + weight: item.weight ?? '—' + } + }) }) + const snapshotClockwise = computed(() => + snapshotTableData.value.filter((row) => row.direction === 0 && row.tier !== 'BIGWIN') + ) + const snapshotCounterclockwise = computed(() => + snapshotTableData.value.filter((row) => row.direction === 1 && row.tier !== 'BIGWIN') + ) + const bigwinTableData = computed(() => + bigwinWeightDisplay.value.map((item) => ({ + grid_number: item.grid, + weight: item.weight + })) + ) + const chartLabels = computed(() => GRID_NUMBERS.map((n) => String(n))) const chartData = computed(() => { const counts = props.record?.result_counts - if (!counts || typeof counts !== 'object') return GRID_NUMBERS.map(() => 0) - return GRID_NUMBERS.map((n) => { - const v = counts[String(n)] ?? counts[n] - return typeof v === 'number' && !Number.isNaN(v) ? v : 0 - }) + if (!counts) return GRID_NUMBERS.map(() => 0) + + // 兼容两种结构:对象 {5:10,...} 或数组 [10, ...] + if (Array.isArray(counts)) { + // 如果是数组,按顺序映射到 GRID_NUMBERS + return GRID_NUMBERS.map((_, idx) => { + const v = counts[idx] + return typeof v === 'number' && !Number.isNaN(v) ? v : 0 + }) + } + + if (typeof counts === 'object') { + return GRID_NUMBERS.map((n) => { + const byString = (counts as Record)[String(n)] + const byNumber = (counts as Record)[n] + const v = byString ?? byNumber + return typeof v === 'number' && !Number.isNaN(v) ? v : 0 + }) + } + + return GRID_NUMBERS.map(() => 0) }) const resultTotal = computed(() => { @@ -385,6 +488,15 @@ .snapshot-table { margin-bottom: 8px; } + .snapshot-group { + margin-bottom: 12px; + } + .snapshot-subtitle { + font-size: 13px; + font-weight: 500; + margin: 4px 0; + color: var(--el-text-color-secondary); + } .chart-wrap { margin-bottom: 8px; } diff --git a/server/app/dice/logic/reward/DiceRewardLogic.php b/server/app/dice/logic/reward/DiceRewardLogic.php index 43bcbaf..f23a3eb 100644 --- a/server/app/dice/logic/reward/DiceRewardLogic.php +++ b/server/app/dice/logic/reward/DiceRewardLogic.php @@ -354,9 +354,13 @@ class DiceRewardLogic if ($configCw !== null) { $tier = isset($configCw['tier']) ? trim((string) $configCw['tier']) : ''; if ($tier !== '') { + // 使用对应奖励配置的 weight 作为格子权重(若未配置则退回最小权重) + $weightCw = isset($configCw['weight']) && $configCw['weight'] !== null + ? $configCw['weight'] + : self::WEIGHT_MIN; $payloadCw = [ 'tier' => $tier, - 'weight' => self::WEIGHT_MIN, + 'weight' => $weightCw, 'grid_number' => $gridNumber, 'start_index' => $startId, 'end_index' => $endIdCw, @@ -374,7 +378,7 @@ class DiceRewardLogic $m->tier = $tier; $m->direction = DiceReward::DIRECTION_CLOCKWISE; $m->end_index = $endIdCw; - $m->weight = self::WEIGHT_MIN; + $m->weight = $weightCw; $m->grid_number = $gridNumber; $m->start_index = $startId; $m->ui_text = $configCw['ui_text'] ?? ''; @@ -390,9 +394,13 @@ class DiceRewardLogic if ($configCcw !== null) { $tier = isset($configCcw['tier']) ? trim((string) $configCcw['tier']) : ''; if ($tier !== '') { + // 使用对应奖励配置的 weight 作为格子权重(若未配置则退回最小权重) + $weightCcw = isset($configCcw['weight']) && $configCcw['weight'] !== null + ? $configCcw['weight'] + : self::WEIGHT_MIN; $payloadCcw = [ 'tier' => $tier, - 'weight' => self::WEIGHT_MIN, + 'weight' => $weightCcw, 'grid_number' => $gridNumber, 'start_index' => $startId, 'end_index' => $endIdCcw, @@ -410,7 +418,7 @@ class DiceRewardLogic $m->tier = $tier; $m->direction = DiceReward::DIRECTION_COUNTERCLOCKWISE; $m->end_index = $endIdCcw; - $m->weight = self::WEIGHT_MIN; + $m->weight = $weightCcw; $m->grid_number = $gridNumber; $m->start_index = $startId; $m->ui_text = $configCcw['ui_text'] ?? ''; diff --git a/server/app/dice/logic/reward_config_record/DiceRewardConfigRecordLogic.php b/server/app/dice/logic/reward_config_record/DiceRewardConfigRecordLogic.php index c4d377a..8d3e6e2 100644 --- a/server/app/dice/logic/reward_config_record/DiceRewardConfigRecordLogic.php +++ b/server/app/dice/logic/reward_config_record/DiceRewardConfigRecordLogic.php @@ -91,40 +91,69 @@ class DiceRewardConfigRecordLogic extends BaseLogic } if (is_array($snapshot) && !empty($snapshot)) { foreach ($snapshot as $item) { - $id = isset($item['id']) ? (int) $item['id'] : 0; + $direction = isset($item['direction']) ? (int) $item['direction'] : null; + $gridNumber = isset($item['grid_number']) ? (int) $item['grid_number'] : 0; $weight = isset($item['weight']) ? (int) $item['weight'] : 1; $weight = max(1, min(10000, $weight)); - if ($id <= 0) { + if (!in_array($direction, [DiceReward::DIRECTION_CLOCKWISE, DiceReward::DIRECTION_COUNTERCLOCKWISE], true)) { + continue; + } + if ($gridNumber <= 0) { continue; } $tier = isset($item['tier']) ? (string) $item['tier'] : ''; if ($tier === '') { - $tier = DiceRewardConfig::where('id', $id)->value('tier'); - $tier = $tier !== null && $tier !== '' ? (string) $tier : ''; + // 若快照中未带 tier,则尝试按方向+点数从现有配置中取 + $tierFromDb = DiceReward::where('direction', $direction)->where('grid_number', $gridNumber)->value('tier'); + $tier = $tierFromDb !== null ? (string) $tierFromDb : ''; } - if ($tier === '') { - continue; - } - // 写入 DiceReward(顺/逆时针同值) - foreach ([DiceReward::DIRECTION_CLOCKWISE, DiceReward::DIRECTION_COUNTERCLOCKWISE] as $direction) { - $affected = DiceReward::where('tier', $tier)->where('direction', $direction)->where('end_index', $id)->update(['weight' => $weight]); - if ($affected === 0) { - $m = new DiceReward(); - $m->tier = $tier; - $m->direction = $direction; - $m->end_index = $id; - $m->weight = $weight; - $m->save(); + // 仅按方向 + 点数更新 DiceReward(若存在则更新,不存在才插入,避免唯一键冲突) + $reward = DiceReward::where('direction', $direction) + ->where('grid_number', $gridNumber) + ->find(); + if ($reward) { + $reward->weight = $weight; + // 若快照中有 tier,补齐 tier 信息 + if ($tier !== '' && (string) $reward->tier !== $tier) { + $reward->tier = $tier; } - } - // BIGWIN:同步写入 DiceRewardConfig.weight - if (strtoupper($tier) === 'BIGWIN') { - DiceRewardConfig::where('id', $id)->update(['weight' => $weight]); + $reward->save(); + } else { + $m = new DiceReward(); + if ($tier !== '') { + $m->tier = $tier; + } + $m->direction = $direction; + $m->grid_number = $gridNumber; + $m->weight = $weight; + $m->save(); } } DiceReward::refreshCache(); } + // 使用记录中的 bigwin_weight JSON 将 BIGWIN 概率导入到 DiceRewardConfig + $recordBigwinWeight = $record['bigwin_weight'] ?? null; + if (is_string($recordBigwinWeight)) { + $decoded = json_decode($recordBigwinWeight, true); + $recordBigwinWeight = is_array($decoded) ? $decoded : null; + } + if (is_array($recordBigwinWeight) && !empty($recordBigwinWeight)) { + foreach ($recordBigwinWeight as $grid => $w) { + $gridNumber = (int) $grid; + $weight = (int) $w; + if ($gridNumber <= 0) { + continue; + } + if ($weight < 0) { + $weight = 0; + } + DiceRewardConfig::where('tier', 'BIGWIN') + ->where('grid_number', $gridNumber) + ->update(['weight' => $weight]); + } + } + $tiers = ['T1', 'T2', 'T3', 'T4', 'T5']; $tierSnapshot = $record['tier_weights_snapshot'] ?? null; if (is_string($tierSnapshot)) { @@ -251,13 +280,15 @@ class DiceRewardConfigRecordLogic extends BaseLogic $paidTierWeights = null; $freeTierWeights = null; + // 来自 DiceReward 的当前权重快照(按方向+点数),用于权重测试模拟 $instance = DiceReward::getCachedInstance(); $byTierDirection = $instance['by_tier_direction'] ?? []; foreach ($byTierDirection as $tier => $byDir) { foreach ($byDir as $dir => $rows) { foreach ($rows as $row) { $snapshot[] = [ - 'id' => (int) ($row['id'] ?? 0), + // 不再记录 DiceReward.id,只记录方向、点数和、档位与权重 + 'direction' => (int) $dir, 'grid_number' => (int) ($row['grid_number'] ?? 0), 'tier' => (string) ($tier ?? ''), 'weight' => (int) ($row['weight'] ?? 0), @@ -266,6 +297,19 @@ class DiceRewardConfigRecordLogic extends BaseLogic } } + // BIGWIN 概率快照从 DiceRewardConfig 读取(例如豹子号配置) + // JSON 结构 {"grid_number": weight, ...} + $bigwinWeights = []; + $bigwinConfigs = DiceRewardConfig::getCachedByTier('BIGWIN'); + foreach ($bigwinConfigs as $cfg) { + $grid = isset($cfg['grid_number']) ? (int) $cfg['grid_number'] : 0; + if ($grid <= 0) { + continue; + } + $w = isset($cfg['weight']) ? (int) $cfg['weight'] : 0; + $bigwinWeights[$grid] = $w; + } + if ($paidConfigId > 0) { $config = DiceLotteryPoolConfig::find($paidConfigId); if (!$config) { @@ -359,6 +403,7 @@ class DiceRewardConfigRecordLogic extends BaseLogic $record->free_tier_weights = $freeTierWeights; $record->result_counts = []; $record->tier_counts = null; + $record->bigwin_weight = $bigwinWeights ?: null; $record->admin_id = $adminId; $record->create_time = date('Y-m-d H:i:s'); $record->save(); diff --git a/server/app/dice/logic/reward_config_record/WeightTestRunner.php b/server/app/dice/logic/reward_config_record/WeightTestRunner.php index fc23ab7..376b633 100644 --- a/server/app/dice/logic/reward_config_record/WeightTestRunner.php +++ b/server/app/dice/logic/reward_config_record/WeightTestRunner.php @@ -196,11 +196,15 @@ class WeightTestRunner */ private function markSuccess(int $recordId, array $resultCounts, array $tierCounts): void { - $platformProfit = DiceRewardConfigRecord::computePlatformProfitFromRelated($recordId); $record = DiceRewardConfigRecord::find($recordId); if ($record) { + // 平台盈利通过关联测试记录统计 + $platformProfit = DiceRewardConfigRecord::computePlatformProfitFromRelated($recordId); + // 落点统计也通过关联测试记录重新统计,避免模拟过程异常导致为空 + $dbResultCounts = DiceRewardConfigRecord::computeResultCountsFromRelated($recordId); + $record->status = DiceRewardConfigRecord::STATUS_SUCCESS; - $record->result_counts = $resultCounts; + $record->result_counts = !empty($dbResultCounts) ? $dbResultCounts : $resultCounts; $record->tier_counts = $tierCounts; $record->remark = null; $record->platform_profit = $platformProfit; diff --git a/server/app/dice/model/reward_config_record/DiceRewardConfigRecord.php b/server/app/dice/model/reward_config_record/DiceRewardConfigRecord.php index 1480b4d..6fa0da1 100644 --- a/server/app/dice/model/reward_config_record/DiceRewardConfigRecord.php +++ b/server/app/dice/model/reward_config_record/DiceRewardConfigRecord.php @@ -37,6 +37,7 @@ use think\model\relation\HasMany; * @property array $result_counts 落点统计 grid_number=>出现次数 * @property array|null $tier_counts 档位出现次数 T1=>count * @property float|null $platform_profit 平台赚取金额(付费抽取次数×100-玩家总收益) + * @property array|null $bigwin_weight 测试时 BIGWIN 档位权重快照(JSON:grid_number=>weight) * @property int|null $admin_id 执行测试的管理员ID * @property string|null $create_time 创建时间 */ @@ -55,7 +56,7 @@ class DiceRewardConfigRecord extends BaseModel protected $table = 'dice_reward_config_record'; - protected $json = ['weight_config_snapshot', 'tier_weights_snapshot', 'result_counts', 'tier_counts', 'paid_tier_weights', 'free_tier_weights']; + protected $json = ['weight_config_snapshot', 'tier_weights_snapshot', 'result_counts', 'tier_counts', 'paid_tier_weights', 'free_tier_weights', 'bigwin_weight']; protected $jsonAssoc = true; @@ -82,4 +83,31 @@ class DiceRewardConfigRecord extends BaseModel ->sum('win_coin'); return round($paidCount * 100 - $sumWinCoin, 2); } + + /** + * 根据关联的 DicePlayRecordTest 统计落点次数 + * result_counts = [grid_number => 出现次数],只统计 roll_number 在 5-30 之间的记录 + * @param int $recordId + * @return array + */ + public static function computeResultCountsFromRelated(int $recordId): array + { + $rows = DicePlayRecordTest::where('reward_config_record_id', $recordId) + ->where('roll_number', '>=', 5) + ->where('roll_number', '<=', 30) + ->field('roll_number, COUNT(*) AS c') + ->group('roll_number') + ->select() + ->toArray(); + + $result = []; + foreach ($rows as $row) { + $grid = (int) ($row['roll_number'] ?? 0); + $cnt = (int) ($row['c'] ?? 0); + if ($grid > 0 && $cnt > 0) { + $result[$grid] = $cnt; + } + } + return $result; + } }