model = new DiceRewardConfig(); } /** * 新增:保存后刷新缓存(权重已迁移至 dice_reward 表) */ public function add(array $data): mixed { $result = parent::add($data); DiceRewardConfig::refreshCache(); return $result; } /** * 修改:保存后刷新缓存;BIGWIN 的 weight 直接写入 dice_reward_config 表,抽奖时从 Config 读取 */ public function edit($id, array $data): mixed { $result = parent::edit($id, $data); DiceRewardConfig::refreshCache(); return $result; } /** * 为列表/分页数据中的 BIGWIN 行附加 weight(来自 DiceReward 缓存) */ public function enrichBigwinWeight(array $listResult): array { $key = isset($listResult['data']) ? 'data' : (isset($listResult['records']) ? 'records' : null); if ($key === null || empty($listResult[$key])) { return $listResult; } $rewardLogic = new DiceRewardLogic(); foreach ($listResult[$key] as $i => $row) { if (isset($row['tier']) && $row['tier'] === 'BIGWIN' && isset($row['grid_number'])) { $listResult[$key][$i]['weight'] = $rewardLogic->getBigwinWeightByGridNumber((int) $row['grid_number']); } } return $listResult; } /** 奖励索引必须为 26 条,id 为 0~25,点数 5~30 各出现一次 */ private const BATCH_INDEX_COUNT = 26; private const INDEX_ID_MIN = 0; private const INDEX_ID_MAX = 25; private const GRID_NUMBER_MIN = 5; private const GRID_NUMBER_MAX = 30; /** * 校验批量更新项(奖励索引表单独立提交,可能只含非 BIGWIN 的若干条) * - 每项必须包含 id、grid_number;grid_number 须在 5~30,提交项内 grid_number 不能重复 * - 若为 26 条则额外校验:id 为 0~25 各一、grid_number 为 5~30 各一 * @return string|null 校验失败返回错误信息,通过返回 null */ public function validateBatchUpdateItems(array $items): ?string { if (count($items) === 0) { return '提交数据不能为空'; } $ids = []; $gridNumbers = []; foreach ($items as $item) { if (! array_key_exists('id', $item) || $item['id'] === null || $item['id'] === '') { return '每项必须包含 id'; } $id = (int) $item['id']; $ids[] = $id; if (! array_key_exists('grid_number', $item)) { return '每项必须包含 grid_number'; } $gn = (int) $item['grid_number']; if ($gn < self::GRID_NUMBER_MIN || $gn > self::GRID_NUMBER_MAX) { return '色子点数 grid_number 只能为 ' . self::GRID_NUMBER_MIN . '~' . self::GRID_NUMBER_MAX . ',当前存在 ' . $gn; } $gridNumbers[] = $gn; } $gridDuplicates = $this->findDuplicateValues($gridNumbers); if ($gridDuplicates !== []) { sort($gridDuplicates); return '色子点数在本批内不能重复,重复的点数为:' . implode('、', $gridDuplicates); } $cnt = count($items); if ($cnt === self::BATCH_INDEX_COUNT) { foreach ($ids as $id) { if ($id < self::INDEX_ID_MIN || $id > self::INDEX_ID_MAX) { return '索引 id 只能为 ' . self::INDEX_ID_MIN . '~' . self::INDEX_ID_MAX . ',当前存在 id=' . $id; } } $idDuplicates = $this->findDuplicateValues($ids); if ($idDuplicates !== []) { sort($idDuplicates); return '索引 id 必须为 0~25 各出现一次不能重复,重复的 id 为:' . implode('、', $idDuplicates); } $requiredIds = range(self::INDEX_ID_MIN, self::INDEX_ID_MAX); if (array_diff($requiredIds, $ids) !== [] || array_diff($ids, $requiredIds) !== []) { return '索引 id 必须且只能为 0~25 各一个'; } $requiredGrid = range(self::GRID_NUMBER_MIN, self::GRID_NUMBER_MAX); if (array_diff($requiredGrid, $gridNumbers) !== [] || array_diff($gridNumbers, $requiredGrid) !== []) { return '色子点数必须且只能为 5~30 各一个'; } } return null; } /** * 找出数组中出现多于一次的值 * @param array $arr * @return array 重复出现的值(去重) */ private function findDuplicateValues(array $arr): array { $counts = array_count_values($arr); $duplicates = []; foreach ($counts as $value => $count) { if ($count > 1) { $duplicates[] = $value; } } return $duplicates; } /** * 批量更新奖励索引配置:grid_number、ui_text、real_ev、tier、remark(不含 weight,BIGWIN 权重单独接口) * @param array $items 每项 [id, grid_number?, ui_text?, real_ev?, tier?, remark?] */ public function batchUpdate(array $items): void { foreach ($items as $row) { if (! array_key_exists('id', $row) || $row['id'] === null || $row['id'] === '') { continue; } $id = (int) $row['id']; $data = []; foreach (['grid_number', 'ui_text', 'ui_text_en', 'real_ev', 'tier', 'remark'] as $field) { if (array_key_exists($field, $row)) { $data[$field] = $row[$field]; } } if (! empty($data)) { parent::edit($id, $data); } } DiceRewardConfig::refreshCache(); } /** * 校验大奖权重提交项:点数 5~30,本批内 grid_number 不能重复 * @return string|null 校验失败返回错误信息(含重复的点数),通过返回 null */ public function validateBigwinWeightItems(array $items): ?string { if (count($items) === 0) { return '提交数据不能为空'; } $gridNumbers = []; foreach ($items as $row) { $gn = isset($row['grid_number']) ? (int) $row['grid_number'] : 0; if ($gn < self::GRID_NUMBER_MIN || $gn > self::GRID_NUMBER_MAX) { return '色子点数 grid_number 只能为 ' . self::GRID_NUMBER_MIN . '~' . self::GRID_NUMBER_MAX . ',当前存在 ' . $gn; } $gridNumbers[] = $gn; } $duplicates = $this->findDuplicateValues($gridNumbers); if ($duplicates !== []) { sort($duplicates); return '大奖权重本批内点数不能重复,重复的点数为:' . implode('、', $duplicates); } return null; } /** * 批量更新 BIGWIN 档位权重(仅写 dice_reward_config 表,不操作 dice_reward) * @param array $items 每项 [grid_number => 5-30, weight => 0-10000] */ public function batchUpdateBigwinWeight(array $items): void { $weightMin = 0; $weightMax = 10000; foreach ($items as $row) { $gridNumber = isset($row['grid_number']) ? (int) $row['grid_number'] : 0; $weight = isset($row['weight']) ? (int) $row['weight'] : 0; if ($gridNumber < 5 || $gridNumber > 30) { continue; } $weight = max($weightMin, min($weightMax, $weight)); $this->model->where('tier', 'BIGWIN') ->where('grid_number', $gridNumber) ->update(['weight' => $weight]); } DiceRewardConfig::refreshCache(); } /** * 删除后刷新缓存 */ public function destroy($ids): bool { $result = parent::destroy($ids); if ($result) { DiceRewardConfig::refreshCache(); } return $result; } /** * 按档位分组返回奖励配置列表(仅配置,权重在 dice_reward 表;权重配比请用 DiceRewardLogic::getListGroupedByTierWithDirection) */ 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; } /** 测试时档位权重均为 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 = []; foreach (['T1', 'T2', 'T3', 'T4', 'T5', 'BIGWIN'] as $t) { $grouped[$t] = $this->model::getCachedByTierForDirection($t, 0); } $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('name', 'default')->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]; } }