[色子游戏]奖励配置权重测试记录

This commit is contained in:
2026-03-11 18:12:19 +08:00
parent 2af7fedcce
commit 064ce06393
18 changed files with 1720 additions and 19 deletions

View File

@@ -30,16 +30,26 @@ class DiceLotteryPoolConfigController extends BaseController
}
/**
* 获取 DiceLotteryPoolConfig 列表数据,仅含 id、name用于 lottery_config_id 下拉(值为 id显示为 name
* 获取 DiceLotteryPoolConfig 列表数据,用于 lottery_config_id 下拉(值为 id显示为 name,并附带 T1-T5 档位权重
* @param Request $request
* @return Response 返回 [ ['id' => int, 'name' => string], ... ]
* @return Response 返回 [ ['id' => int, 'name' => string, 't1_weight' => int, ... 't5_weight' => int], ... ]
*/
#[Permission('色子奖池配置列表', 'dice:lottery_pool_config:index:index')]
public function getOptions(Request $request): Response
{
$list = DiceLotteryPoolConfig::field('id,name')->order('id', 'asc')->select();
$list = DiceLotteryPoolConfig::field('id,name,t1_weight,t2_weight,t3_weight,t4_weight,t5_weight')
->order('id', 'asc')
->select();
$data = $list->map(function ($item) {
return ['id' => (int) $item['id'], 'name' => (string) ($item['name'] ?? '')];
return [
'id' => (int) $item['id'],
'name' => (string) ($item['name'] ?? ''),
't1_weight' => (int) ($item['t1_weight'] ?? 0),
't2_weight' => (int) ($item['t2_weight'] ?? 0),
't3_weight' => (int) ($item['t3_weight'] ?? 0),
't4_weight' => (int) ($item['t4_weight'] ?? 0),
't5_weight' => (int) ($item['t5_weight'] ?? 0),
];
})->toArray();
return $this->success($data);
}

View File

@@ -155,4 +155,29 @@ class DiceRewardConfigController extends BaseController
return $this->fail($e->getMessage());
}
}
/**
* 权重配比测试:仅模拟落点统计,不创建游玩记录。按当前配置在内存中模拟 N 次抽奖,返回各 grid_number 落点次数,可选保存到 dice_reward_config_record。
* @param Request $request test_count: 100|500|1000, save_record: bool, lottery_config_id: int|null 奖池配置ID用于设定 T1-T5 概率
* @return Response
*/
#[Permission('奖励配置列表', 'dice:reward_config:index:index')]
public function runWeightTest(Request $request): Response
{
$testCount = (int) $request->post('test_count', 100);
$saveRecord = (bool) $request->post('save_record', true);
$adminId = isset($this->adminInfo['id']) ? (int) $this->adminInfo['id'] : null;
$lotteryConfigId = $request->post('lottery_config_id', null);
if ($lotteryConfigId !== null && $lotteryConfigId !== '') {
$lotteryConfigId = (int) $lotteryConfigId;
} else {
$lotteryConfigId = null;
}
try {
$result = $this->logic->runWeightTest($testCount, $saveRecord, $adminId, $lotteryConfigId);
return $this->success($result);
} catch (\plugin\saiadmin\exception\ApiException $e) {
return $this->fail($e->getMessage());
}
}
}

View File

@@ -0,0 +1,168 @@
<?php
// +----------------------------------------------------------------------
// | saiadmin [ saiadmin快速开发框架 ]
// +----------------------------------------------------------------------
// | Author: your name
// +----------------------------------------------------------------------
namespace app\dice\controller\reward_config_record;
use app\dice\logic\reward_config_record\DiceRewardConfigRecordLogic;
use app\dice\validate\reward_config_record\DiceRewardConfigRecordValidate;
use plugin\saiadmin\basic\BaseController;
use plugin\saiadmin\app\model\system\SystemUser;
use plugin\saiadmin\service\Permission;
use support\Request;
use support\Response;
/**
* 奖励配置权重测试记录控制器
*/
class DiceRewardConfigRecordController extends BaseController
{
/**
* 构造函数
*/
public function __construct()
{
$this->logic = new DiceRewardConfigRecordLogic();
$this->validate = new DiceRewardConfigRecordValidate;
parent::__construct();
}
/**
* 数据列表
* @param Request $request
* @return Response
*/
#[Permission('奖励配置权重测试记录列表', 'dice:reward_config_record:index:index')]
public function index(Request $request): Response
{
$where = $request->more([
]);
$query = $this->logic->search($where);
$data = $this->logic->getList($query);
return $this->success($data);
}
/**
* 读取数据
* @param Request $request
* @return Response
*/
#[Permission('奖励配置权重测试记录读取', 'dice:reward_config_record:index:read')]
public function read(Request $request): Response
{
$id = $request->input('id', '');
$model = $this->logic->read($id);
if ($model) {
$data = is_array($model) ? $model : $model->toArray();
$data['admin_name'] = $this->getAdminName((int) ($data['admin_id'] ?? 0));
return $this->success($data);
} else {
return $this->fail('未查找到信息');
}
}
/**
* 根据管理员 ID 获取姓名realname 优先,否则 username
*/
private function getAdminName(int $adminId): string
{
if ($adminId <= 0) {
return '—';
}
$user = SystemUser::where('id', $adminId)->field('id,realname,username')->find();
if (!$user) {
return '';
}
$user = is_array($user) ? $user : $user->toArray();
$name = trim((string) ($user['realname'] ?? ''));
if ($name !== '') {
return $name;
}
$name = trim((string) ($user['username'] ?? ''));
return $name !== '' ? $name : (string) $adminId;
}
/**
* 保存数据
* @param Request $request
* @return Response
*/
#[Permission('奖励配置权重测试记录添加', 'dice:reward_config_record:index:save')]
public function save(Request $request): Response
{
$data = $request->post();
$this->validate('save', $data);
$result = $this->logic->add($data);
if ($result) {
return $this->success('添加成功');
} else {
return $this->fail('添加失败');
}
}
/**
* 更新数据
* @param Request $request
* @return Response
*/
#[Permission('奖励配置权重测试记录修改', 'dice:reward_config_record:index:update')]
public function update(Request $request): Response
{
$data = $request->post();
$this->validate('update', $data);
$result = $this->logic->edit($data['id'], $data);
if ($result) {
return $this->success('修改成功');
} else {
return $this->fail('修改失败');
}
}
/**
* 删除数据
* @param Request $request
* @return Response
*/
#[Permission('奖励配置权重测试记录删除', 'dice:reward_config_record:index:destroy')]
public function destroy(Request $request): Response
{
$ids = $request->post('ids', '');
if (empty($ids)) {
return $this->fail('请选择要删除的数据');
}
$result = $this->logic->destroy($ids);
if ($result) {
return $this->success('删除成功');
} else {
return $this->fail('删除失败');
}
}
/**
* 导入:将测试记录的权重写入 DiceRewardConfig 与 DiceLotteryPoolConfig并重新实例化缓存
* @param Request $request record_id: 测试记录ID, lottery_config_id: 可选导入档位权重到的奖池配置ID不传则用记录内的 lottery_config_id
* @return Response
*/
#[Permission('奖励配置权重测试记录列表', 'dice:reward_config_record:index:index')]
public function importFromRecord(Request $request): Response
{
$recordId = (int) $request->post('record_id', 0);
$lotteryConfigId = $request->post('lottery_config_id', null);
if ($recordId <= 0) {
return $this->fail('请指定测试记录');
}
if ($lotteryConfigId !== null && $lotteryConfigId !== '') {
$lotteryConfigId = (int) $lotteryConfigId;
} else {
$lotteryConfigId = null;
}
try {
$this->logic->importFromRecord($recordId, $lotteryConfigId);
return $this->success('导入成功,已刷新奖励配置与奖池配置');
} catch (\plugin\saiadmin\exception\ApiException $e) {
return $this->fail($e->getMessage());
}
}
}

View File

@@ -6,10 +6,12 @@
// +----------------------------------------------------------------------
namespace app\dice\logic\reward_config;
use app\dice\model\lottery_pool_config\DiceLotteryPoolConfig;
use app\dice\model\reward_config\DiceRewardConfig;
use app\dice\model\reward_config\DiceRewardConfigRecord;
use plugin\saiadmin\basic\think\BaseLogic;
use plugin\saiadmin\exception\ApiException;
use plugin\saiadmin\utils\Helper;
use app\dice\model\reward_config\DiceRewardConfig;
use support\Log;
/**
@@ -135,4 +137,160 @@ class DiceRewardConfigLogic extends BaseLogic
}
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 奖池配置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 = $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];
}
}

View File

@@ -0,0 +1,125 @@
<?php
// +----------------------------------------------------------------------
// | saiadmin [ saiadmin快速开发框架 ]
// +----------------------------------------------------------------------
namespace app\dice\logic\reward_config_record;
use app\dice\model\lottery_pool_config\DiceLotteryPoolConfig;
use app\dice\model\reward_config\DiceRewardConfig;
use app\dice\model\reward_config_record\DiceRewardConfigRecord;
use plugin\saiadmin\basic\think\BaseLogic;
use plugin\saiadmin\exception\ApiException;
use plugin\saiadmin\app\model\system\SystemUser;
/**
* 奖励配置权重测试记录逻辑层
*/
class DiceRewardConfigRecordLogic extends BaseLogic
{
public function __construct()
{
$this->model = new DiceRewardConfigRecord();
}
/**
* 分页列表,并为每条记录附加 admin_name管理员姓名realname 或 username
*/
public function getList($query): mixed
{
$result = parent::getList($query);
if (!is_array($result)) {
return $result;
}
$rows = $result['data'] ?? $result['records'] ?? null;
if (!is_array($rows) || empty($rows)) {
return $result;
}
$adminIds = array_unique(array_filter(array_column($rows, 'admin_id')));
$nameMap = $this->getAdminNameMap($adminIds);
$key = isset($result['data']) ? 'data' : 'records';
foreach ($result[$key] as &$row) {
$aid = isset($row['admin_id']) ? (int) $row['admin_id'] : 0;
$row['admin_name'] = $nameMap[$aid] ?? ($aid > 0 ? '' : '—');
}
unset($row);
return $result;
}
/**
* 根据管理员 ID 列表获取 id => 姓名realname 优先,否则 username
* @param array $adminIds
* @return array<int, string>
*/
private function getAdminNameMap(array $adminIds): array
{
if (empty($adminIds)) {
return [];
}
$list = SystemUser::whereIn('id', $adminIds)->field('id,realname,username')->select()->toArray();
$map = [];
foreach ($list as $user) {
$user = is_array($user) ? $user : (array) $user;
$id = (int) ($user['id'] ?? 0);
$name = trim((string) ($user['realname'] ?? ''));
if ($name === '') {
$name = trim((string) ($user['username'] ?? ''));
}
$map[$id] = $name !== '' ? $name : (string) $id;
}
return $map;
}
/**
* 将测试记录的权重导入到正式配置weight_config_snapshot → DiceRewardConfigtier_weights_snapshot → DiceLotteryPoolConfig并刷新缓存
* @param int $recordId 测试记录 ID
* @param int|null $lotteryConfigId 要导入档位权重的奖池配置 ID不传则使用记录中的 lottery_config_id若有
* @throws ApiException
*/
public function importFromRecord(int $recordId, ?int $lotteryConfigId = null): void
{
$record = $this->model->find($recordId);
if (!$record) {
throw new ApiException('测试记录不存在');
}
$record = is_array($record) ? $record : $record->toArray();
$snapshot = $record['weight_config_snapshot'] ?? null;
if (is_string($snapshot)) {
$snapshot = json_decode($snapshot, true);
}
if (is_array($snapshot) && !empty($snapshot)) {
foreach ($snapshot as $item) {
$id = isset($item['id']) ? (int) $item['id'] : 0;
$weight = isset($item['weight']) ? (int) $item['weight'] : 1;
if ($id > 0) {
DiceRewardConfig::where('id', $id)->update(['weight' => $weight]);
}
}
}
$tierSnapshot = $record['tier_weights_snapshot'] ?? null;
if (is_string($tierSnapshot)) {
$tierSnapshot = json_decode($tierSnapshot, true);
}
$targetLotteryId = $lotteryConfigId !== null && $lotteryConfigId > 0
? $lotteryConfigId
: (isset($record['lottery_config_id']) && (int) $record['lottery_config_id'] > 0 ? (int) $record['lottery_config_id'] : null);
if (is_array($tierSnapshot) && !empty($tierSnapshot) && $targetLotteryId > 0) {
$pool = DiceLotteryPoolConfig::find($targetLotteryId);
if (!$pool) {
throw new ApiException('奖池配置不存在');
}
$update = [
't1_weight' => (int) ($tierSnapshot['T1'] ?? $tierSnapshot['t1'] ?? 0),
't2_weight' => (int) ($tierSnapshot['T2'] ?? $tierSnapshot['t2'] ?? 0),
't3_weight' => (int) ($tierSnapshot['T3'] ?? $tierSnapshot['t3'] ?? 0),
't4_weight' => (int) ($tierSnapshot['T4'] ?? $tierSnapshot['t4'] ?? 0),
't5_weight' => (int) ($tierSnapshot['T5'] ?? $tierSnapshot['t5'] ?? 0),
];
DiceLotteryPoolConfig::where('id', $targetLotteryId)->update($update);
}
DiceRewardConfig::refreshCache();
DiceRewardConfig::clearRequestInstance();
}
}

View File

@@ -0,0 +1,32 @@
<?php
// +----------------------------------------------------------------------
// | saiadmin [ saiadmin快速开发框架 ]
// +----------------------------------------------------------------------
namespace app\dice\model\reward_config;
use plugin\saiadmin\basic\think\BaseModel;
/**
* 权重配比测试记录模型
*
* dice_reward_config_record 保存测试时的权重快照与落点统计
*
* @property int $id
* @property int $test_count 测试次数 100/500/1000/5000/10000
* @property array $weight_config_snapshot 测试时权重配比快照
* @property array $tier_weights_snapshot 测试时 T1-T5 档位权重快照(来自奖池配置)
* @property int|null $lottery_config_id 测试时使用的奖池配置 ID
* @property array $result_counts 落点统计 grid_number=>出现次数
* @property int|null $admin_id 执行测试的管理员ID
* @property string|null $create_time 创建时间
*/
class DiceRewardConfigRecord extends BaseModel
{
protected $pk = 'id';
protected $table = 'dice_reward_config_record';
protected $json = ['weight_config_snapshot', 'tier_weights_snapshot', 'result_counts'];
protected $jsonAssoc = true;
}

View File

@@ -0,0 +1,34 @@
<?php
// +----------------------------------------------------------------------
// | saiadmin [ saiadmin快速开发框架 ]
// +----------------------------------------------------------------------
// | Author: your name
// +----------------------------------------------------------------------
namespace app\dice\model\reward_config_record;
use plugin\saiadmin\basic\think\BaseModel;
/**
* 奖励配置权重测试记录模型
*
* dice_reward_config_record 奖励配置权重测试记录
*
* @property int $id 主键
* @property int $test_count 测试次数 100/500/1000/5000/10000
* @property array $weight_config_snapshot 测试时权重配比快照:按档位 id,grid_number,tier,weight
* @property array $tier_weights_snapshot 测试时 T1-T5 档位权重快照(来自奖池配置)
* @property int|null $lottery_config_id 测试时使用的奖池配置 ID
* @property array $result_counts 落点统计 grid_number=>出现次数
* @property int|null $admin_id 执行测试的管理员ID
* @property string|null $create_time 创建时间
*/
class DiceRewardConfigRecord extends BaseModel
{
protected $pk = 'id';
protected $table = 'dice_reward_config_record';
protected $json = ['weight_config_snapshot', 'tier_weights_snapshot', 'result_counts'];
protected $jsonAssoc = true;
}

View File

@@ -0,0 +1,42 @@
<?php
// +----------------------------------------------------------------------
// | saiadmin [ saiadmin快速开发框架 ]
// +----------------------------------------------------------------------
// | Author: your name
// +----------------------------------------------------------------------
namespace app\dice\validate\reward_config_record;
use plugin\saiadmin\basic\BaseValidate;
/**
* 奖励配置权重测试记录验证器
*/
class DiceRewardConfigRecordValidate extends BaseValidate
{
/**
* 定义验证规则
*/
protected $rule = [
'test_count' => 'require',
];
/**
* 定义错误信息
*/
protected $message = [
'test_count' => '测试次数100/500/1000必须填写',
];
/**
* 定义场景
*/
protected $scene = [
'save' => [
'test_count',
],
'update' => [
'test_count',
],
];
}