model = new DiceRewardConfig(); } /** * 新增:weight 限制 1-10000;保存后刷新缓存 */ public function add(array $data): mixed { $data = $this->normalizeWeight($data); $result = parent::add($data); DiceRewardConfig::refreshCache(); return $result; } /** * 修改:weight 限制 1-10000;保存后刷新缓存 */ public function edit($id, array $data): mixed { $data = $this->normalizeWeight($data); $result = parent::edit($id, $data); DiceRewardConfig::refreshCache(); return $result; } /** * 删除后刷新缓存 */ public function destroy($ids): bool { $result = parent::destroy($ids); if ($result) { DiceRewardConfig::refreshCache(); } return $result; } /** * weight 限制 1-10000 */ private function normalizeWeight(array $data): array { $w = isset($data['weight']) ? (int) $data['weight'] : self::WEIGHT_MIN; $data['weight'] = max(self::WEIGHT_MIN, min(self::WEIGHT_MAX, $w)); return $data; } /** * 按档位分组返回奖励配置列表(用于 T1-T5、BIGWIN 权重配比) * @return array 键为 T1|T2|T3|T4|T5|BIGWIN,值为该档位下的配置行数组 */ public function getListGroupedByTier(): array { $tiers = ['T1', 'T2', 'T3', 'T4', 'T5', 'BIGWIN']; $list = $this->model->whereIn('tier', $tiers)->order('tier')->order('id')->select()->toArray(); $grouped = []; foreach ($tiers as $t) { $grouped[$t] = []; } foreach ($list as $row) { $tier = isset($row['tier']) ? (string) $row['tier'] : ''; if ($tier !== '' && isset($grouped[$tier])) { $grouped[$tier][] = $row; } } return $grouped; } /** * 批量更新权重:单条 weight 1-10000,各档位权重和不限制 * @param array $items 元素为 [ id => 配置ID, weight => 1-10000 ] * @throws ApiException 当单条 weight 非法时 */ public function batchUpdateWeights(array $items): void { if (empty($items)) { return; } $items = array_values($items); $ids = []; $weightById = []; foreach ($items as $item) { if (!is_array($item)) { continue; } $id = isset($item['id']) ? (int) $item['id'] : 0; $w = isset($item['weight']) ? (int) $item['weight'] : self::WEIGHT_MIN; if ($id < 0) { throw new ApiException('存在无效的配置ID'); } if ($w < self::WEIGHT_MIN || $w > self::WEIGHT_MAX) { throw new ApiException('权重必须在 ' . self::WEIGHT_MIN . '-' . self::WEIGHT_MAX . ' 之间'); } $ids[] = $id; $weightById[$id] = $w; } $list = $this->model->whereIn('id', array_unique($ids))->field('id,tier,grid_number')->select()->toArray(); $idToTier = []; foreach ($list as $r) { $id = isset($r['id']) ? (int) $r['id'] : 0; $idToTier[$id] = isset($r['tier']) ? (string) $r['tier'] : ''; } foreach ($weightById as $id => $w) { $tier = $idToTier[$id] ?? ''; if ($tier === '') { throw new ApiException('配置ID ' . $id . ' 不存在或档位为空'); } DiceRewardConfig::where('id', $id)->update(['weight' => $w]); } DiceRewardConfig::refreshCache(); } /** 测试时档位权重均为 0 的异常标识 */ private const EXCEPTION_WEIGHT_ALL_ZERO = 'REWARD_WEIGHT_ALL_ZERO'; /** * 按权重抽取一条配置(与 PlayStartLogic 抽奖逻辑一致,仅 weight>0 参与) */ private static function drawRewardByWeight(array $rewards): array { if (empty($rewards)) { throw new \InvalidArgumentException('rewards 不能为空'); } $candidateWeights = []; foreach ($rewards as $i => $row) { $w = isset($row['weight']) ? (float) $row['weight'] : 0.0; if ($w > 0) { $candidateWeights[$i] = $w; } } $total = (float) array_sum($candidateWeights); if ($total > 0) { $r = (random_int(0, PHP_INT_MAX - 1) / (float) PHP_INT_MAX) * $total; $acc = 0.0; foreach ($candidateWeights as $i => $w) { $acc += $w; if ($r < $acc) { return $rewards[$i]; } } return $rewards[array_key_last($candidateWeights)]; } throw new \RuntimeException(self::EXCEPTION_WEIGHT_ALL_ZERO); } /** * 按档位权重数组抽取 T1-T5 */ private static function drawTierByWeightArray(array $tiers, array $weights): string { $total = array_sum($weights); if ($total <= 0) { return $tiers[random_int(0, count($tiers) - 1)]; } $r = random_int(1, (int) $total); $acc = 0; foreach ($weights as $i => $w) { $acc += (int) $w; if ($r <= $acc) { return $tiers[$i]; } } return $tiers[count($tiers) - 1]; } /** * 运行权重配比测试:仅按当前配置在内存中模拟 N 次抽奖,统计各 grid_number 落点数量。 * 不创建任何游玩记录(DicePlayRecord)、不扣券、不写钱包,仅用于验证权重配比效果。 * * @param int $testCount 测试次数 100/500/1000/5000/10000 * @param bool $saveRecord 是否保存到 dice_reward_config_record(测试记录表,非游玩记录) * @param int|null $adminId 执行人管理员ID * @param int|null $lotteryConfigId 奖池配置ID(DiceLotteryPoolConfig),用于设定 T1-T5 档位概率;不传则使用 type=0 的配置或均等 * @return array{counts: array, record_id: int|null} counts 为 grid_number=>出现次数 */ public function runWeightTest(int $testCount, bool $saveRecord = true, ?int $adminId = null, ?int $lotteryConfigId = null): array { $allowedCounts = [100, 500, 1000, 5000, 10000]; if (!in_array($testCount, $allowedCounts, true)) { throw new ApiException('测试次数仅支持 100、500、1000、5000、10000'); } $grouped = $this->getListGroupedByTier(); $tiers = ['T1', 'T2', 'T3', 'T4', 'T5']; $tierWeights = [1, 1, 1, 1, 1]; $config = null; if ($lotteryConfigId !== null && $lotteryConfigId > 0) { $config = DiceLotteryPoolConfig::find($lotteryConfigId); } if (!$config) { $config = DiceLotteryPoolConfig::where('type', 0)->find(); } if ($config) { $tierWeights = [ (int) ($config->t1_weight ?? 0), (int) ($config->t2_weight ?? 0), (int) ($config->t3_weight ?? 0), (int) ($config->t4_weight ?? 0), (int) ($config->t5_weight ?? 0), ]; if (array_sum($tierWeights) <= 0) { $tierWeights = [1, 1, 1, 1, 1]; } } $counts = []; $maxRetry = 20; for ($i = 0; $i < $testCount; $i++) { $tier = self::drawTierByWeightArray($tiers, $tierWeights); $rewards = $grouped[$tier] ?? []; if (empty($rewards)) { continue; } $attempt = 0; while ($attempt < $maxRetry) { try { $chosen = self::drawRewardByWeight($rewards); $gridNumber = isset($chosen['grid_number']) ? (int) $chosen['grid_number'] : 0; if ($gridNumber >= 5 && $gridNumber <= 30) { $counts[$gridNumber] = ($counts[$gridNumber] ?? 0) + 1; } break; } catch (\RuntimeException $e) { if ($e->getMessage() === self::EXCEPTION_WEIGHT_ALL_ZERO) { $attempt++; continue; } throw $e; } } } $snapshot = []; foreach ($grouped as $tierKey => $rows) { foreach ($rows as $row) { $snapshot[] = [ 'id' => (int) ($row['id'] ?? 0), 'grid_number' => (int) ($row['grid_number'] ?? 0), 'tier' => (string) ($row['tier'] ?? ''), 'weight' => (int) ($row['weight'] ?? 0), ]; } } $tierWeightsSnapshot = [ 'T1' => $tierWeights[0] ?? 0, 'T2' => $tierWeights[1] ?? 0, 'T3' => $tierWeights[2] ?? 0, 'T4' => $tierWeights[3] ?? 0, 'T5' => $tierWeights[4] ?? 0, ]; $recordId = null; if ($saveRecord) { $record = new DiceRewardConfigRecord(); $record->test_count = $testCount; $record->weight_config_snapshot = $snapshot; $record->tier_weights_snapshot = $tierWeightsSnapshot; $record->lottery_config_id = $config ? (int) $config->id : null; $record->result_counts = $counts; $record->admin_id = $adminId; $record->create_time = date('Y-m-d H:i:s'); $record->save(); $recordId = (int) $record->id; } return ['counts' => $counts, 'record_id' => $recordId]; } }