Files
dafuweng-saiadmin6.x/server/app/dice/logic/reward_config/DiceRewardConfigLogic.php
zhenhui 05d592dcbc 1.优化接口返回中间信息ui_text(中文)ui_text_en(英文)
2.修复BUG色子点数权重配置-权重配比-顺时针/逆时针显示错误
2026-03-13 17:50:57 +08:00

414 lines
16 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?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 为 025点数 530 各出现一次 */
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_numbergrid_number 须在 530提交项内 grid_number 不能重复
* - 若为 26 条则额外校验id 为 025 各一、grid_number 为 530 各一
* @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 必须为 025 各出现一次不能重复,重复的 id 为:' . implode('、', $idDuplicates);
}
$requiredIds = range(self::INDEX_ID_MIN, self::INDEX_ID_MAX);
if (array_diff($requiredIds, $ids) !== [] || array_diff($ids, $requiredIds) !== []) {
return '索引 id 必须且只能为 025 各一个';
}
$requiredGrid = range(self::GRID_NUMBER_MIN, self::GRID_NUMBER_MAX);
if (array_diff($requiredGrid, $gridNumbers) !== [] || array_diff($gridNumbers, $requiredGrid) !== []) {
return '色子点数必须且只能为 530 各一个';
}
}
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不含 weightBIGWIN 权重单独接口)
* @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();
}
/**
* 校验大奖权重提交项:点数 530本批内 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 奖池配置IDDiceLotteryPoolConfig用于设定 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];
}
}