414 lines
16 KiB
PHP
414 lines
16 KiB
PHP
<?php
|
||
// +----------------------------------------------------------------------
|
||
// | saiadmin [ saiadmin快速开发框架 ]
|
||
// +----------------------------------------------------------------------
|
||
// | Author: your name
|
||
// +----------------------------------------------------------------------
|
||
namespace app\dice\logic\reward_config;
|
||
|
||
use app\dice\logic\reward\DiceRewardLogic;
|
||
use app\dice\model\lottery_pool_config\DiceLotteryPoolConfig;
|
||
use app\dice\model\reward\DiceRewardConfig;
|
||
use app\dice\model\reward_config_record\DiceRewardConfigRecord;
|
||
use plugin\saiadmin\basic\think\BaseLogic;
|
||
use plugin\saiadmin\exception\ApiException;
|
||
use plugin\saiadmin\utils\Helper;
|
||
use support\Log;
|
||
|
||
/**
|
||
* 奖励配置逻辑层(DiceRewardConfig)
|
||
* weight 1-10000,各档位权重和不限制
|
||
*/
|
||
class DiceRewardConfigLogic extends BaseLogic
|
||
{
|
||
/** weight 取值范围 */
|
||
private const WEIGHT_MIN = 1;
|
||
private const WEIGHT_MAX = 10000;
|
||
|
||
public function __construct()
|
||
{
|
||
$this->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', '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<int,int>, 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('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];
|
||
}
|
||
}
|