From 064ce06393dd5fd6255007a4d5562b4f1414d8a7 Mon Sep 17 00:00:00 2001 From: zhenhui <1276357500@qq.com> Date: Wed, 11 Mar 2026 18:12:19 +0800 Subject: [PATCH] =?UTF-8?q?[=E8=89=B2=E5=AD=90=E6=B8=B8=E6=88=8F]=E5=A5=96?= =?UTF-8?q?=E5=8A=B1=E9=85=8D=E7=BD=AE=E6=9D=83=E9=87=8D=E6=B5=8B=E8=AF=95?= =?UTF-8?q?=E8=AE=B0=E5=BD=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dice/api/lottery_pool_config/index.ts | 47 ++- .../plugin/dice/api/reward_config/index.ts | 17 + .../dice/api/reward_config_record/index.ts | 77 +++++ .../plugin/dice/reward_config/index/index.vue | 12 + .../index/modules/weight-test-dialog.vue | 227 ++++++++++++ .../dice/reward_config_record/index/index.vue | 217 ++++++++++++ .../index/modules/detail-drawer.vue | 323 ++++++++++++++++++ .../index/modules/edit-dialog.vue | 150 ++++++++ .../index/modules/table-search.vue | 62 ++++ .../DiceLotteryPoolConfigController.php | 18 +- .../DiceRewardConfigController.php | 25 ++ .../DiceRewardConfigRecordController.php | 168 +++++++++ .../reward_config/DiceRewardConfigLogic.php | 160 ++++++++- .../DiceRewardConfigRecordLogic.php | 125 +++++++ .../reward_config/DiceRewardConfigRecord.php | 32 ++ .../DiceRewardConfigRecord.php | 34 ++ .../DiceRewardConfigRecordValidate.php | 42 +++ server/plugin/saiadmin/config/route.php | 3 + 18 files changed, 1720 insertions(+), 19 deletions(-) create mode 100644 saiadmin-artd/src/views/plugin/dice/api/reward_config_record/index.ts create mode 100644 saiadmin-artd/src/views/plugin/dice/reward_config/index/modules/weight-test-dialog.vue create mode 100644 saiadmin-artd/src/views/plugin/dice/reward_config_record/index/index.vue create mode 100644 saiadmin-artd/src/views/plugin/dice/reward_config_record/index/modules/detail-drawer.vue create mode 100644 saiadmin-artd/src/views/plugin/dice/reward_config_record/index/modules/edit-dialog.vue create mode 100644 saiadmin-artd/src/views/plugin/dice/reward_config_record/index/modules/table-search.vue create mode 100644 server/app/dice/controller/reward_config_record/DiceRewardConfigRecordController.php create mode 100644 server/app/dice/logic/reward_config_record/DiceRewardConfigRecordLogic.php create mode 100644 server/app/dice/model/reward_config/DiceRewardConfigRecord.php create mode 100644 server/app/dice/model/reward_config_record/DiceRewardConfigRecord.php create mode 100644 server/app/dice/validate/reward_config_record/DiceRewardConfigRecordValidate.php diff --git a/saiadmin-artd/src/views/plugin/dice/api/lottery_pool_config/index.ts b/saiadmin-artd/src/views/plugin/dice/api/lottery_pool_config/index.ts index b24ad16..104f75e 100644 --- a/saiadmin-artd/src/views/plugin/dice/api/lottery_pool_config/index.ts +++ b/saiadmin-artd/src/views/plugin/dice/api/lottery_pool_config/index.ts @@ -11,23 +11,42 @@ export default { */ list(params: Record) { return request.get({ - url: '/dice/lottery_pool_config/DiceLotteryPoolConfig/index', + url: '/core/dice/lottery_pool_config/DiceLotteryPoolConfig/index', params }) }, /** * 获取 DiceLotteryPoolConfig 列表数据,仅含 id、name,用于 lottery_config_id 下拉 - * @returns DiceLotteryPoolConfig['id','name'] 列表 + * @returns DiceLotteryPoolConfig['id','name','t1_weight'..'t5_weight'] 列表 */ - async getOptions(): Promise> { + async getOptions(): Promise< + Array<{ + id: number + name: string + t1_weight: number + t2_weight: number + t3_weight: number + t4_weight: number + t5_weight: number + }> + > { const res = await request.get({ - url: '/dice/lottery_pool_config/DiceLotteryPoolConfig/getOptions' + url: '/core/dice/lottery_pool_config/DiceLotteryPoolConfig/getOptions' }) - const rows = (res?.data ?? []) as Array<{ id: number; name: string }> - return Array.isArray(rows) - ? rows.map((r) => ({ id: Number(r.id), name: String(r.name ?? r.id ?? '') })) - : [] + // 兼容:request.get 通常返回后端 success(data) 的 data(数组);部分环境可能返回整包 { data: [] } + const rows = Array.isArray(res) ? res : (Array.isArray((res as any)?.data) ? (res as any).data : []) + + if (!Array.isArray(rows)) return [] + return rows.map((r: any) => ({ + id: Number(r.id), + name: String(r.name ?? r.id ?? ''), + t1_weight: Number(r.t1_weight ?? 0), + t2_weight: Number(r.t2_weight ?? 0), + t3_weight: Number(r.t3_weight ?? 0), + t4_weight: Number(r.t4_weight ?? 0), + t5_weight: Number(r.t5_weight ?? 0) + })) }, /** @@ -37,7 +56,7 @@ export default { */ read(id: number | string) { return request.get({ - url: '/dice/lottery_pool_config/DiceLotteryPoolConfig/read?id=' + id + url: '/core/dice/lottery_pool_config/DiceLotteryPoolConfig/read?id=' + id }) }, @@ -48,7 +67,7 @@ export default { */ save(params: Record) { return request.post({ - url: '/dice/lottery_pool_config/DiceLotteryPoolConfig/save', + url: '/core/dice/lottery_pool_config/DiceLotteryPoolConfig/save', data: params }) }, @@ -60,7 +79,7 @@ export default { */ update(params: Record) { return request.put({ - url: '/dice/lottery_pool_config/DiceLotteryPoolConfig/update', + url: '/core/dice/lottery_pool_config/DiceLotteryPoolConfig/update', data: params }) }, @@ -72,7 +91,7 @@ export default { */ delete(params: Record) { return request.del({ - url: '/dice/lottery_pool_config/DiceLotteryPoolConfig/destroy', + url: '/core/dice/lottery_pool_config/DiceLotteryPoolConfig/destroy', data: params }) }, @@ -92,7 +111,7 @@ export default { t5_weight: number profit_amount: number }>({ - url: '/dice/lottery_pool_config/DiceLotteryPoolConfig/getCurrentPool' + url: '/core/dice/lottery_pool_config/DiceLotteryPoolConfig/getCurrentPool' }) }, @@ -108,7 +127,7 @@ export default { t5_weight?: number }) { return request.post({ - url: '/dice/lottery_pool_config/DiceLotteryPoolConfig/updateCurrentPool', + url: '/core/dice/lottery_pool_config/DiceLotteryPoolConfig/updateCurrentPool', data: params }) } diff --git a/saiadmin-artd/src/views/plugin/dice/api/reward_config/index.ts b/saiadmin-artd/src/views/plugin/dice/api/reward_config/index.ts index 11fafaa..ec74042 100644 --- a/saiadmin-artd/src/views/plugin/dice/api/reward_config/index.ts +++ b/saiadmin-artd/src/views/plugin/dice/api/reward_config/index.ts @@ -80,5 +80,22 @@ export default { url: '/core/dice/reward_config/DiceRewardConfig/batchUpdateWeights', data: { items } }) + }, + + /** + * 权重配比测试:按当前配置模拟 N 次抽奖,返回各 grid_number 落点次数 + * @param test_count 100 | 500 | 1000 + * @param save_record 是否保存到 dice_reward_config_record + * @param lottery_config_id 奖池配置ID(DiceLotteryPoolConfig),用于设定 T1-T5 档位概率;不传则使用 type=0 或均等 + */ + runWeightTest(params: { + test_count: number + save_record?: boolean + lottery_config_id?: number | null + }) { + return request.post<{ data: { counts: Record; record_id: number | null } }>({ + url: '/core/dice/reward_config/DiceRewardConfig/runWeightTest', + data: params + }) } } diff --git a/saiadmin-artd/src/views/plugin/dice/api/reward_config_record/index.ts b/saiadmin-artd/src/views/plugin/dice/api/reward_config_record/index.ts new file mode 100644 index 0000000..b2c77ca --- /dev/null +++ b/saiadmin-artd/src/views/plugin/dice/api/reward_config_record/index.ts @@ -0,0 +1,77 @@ +import request from '@/utils/http' + +/** + * 奖励配置权重测试记录 API接口 + */ +export default { + /** + * 获取数据列表 + * @param params 搜索参数 + * @returns 数据列表 + */ + list(params: Record) { + return request.get({ + url: '/dice/reward_config_record/DiceRewardConfigRecord/index', + params + }) + }, + + /** + * 读取数据 + * @param id 数据ID + * @returns 数据详情 + */ + read(id: number | string) { + return request.get({ + url: '/dice/reward_config_record/DiceRewardConfigRecord/read?id=' + id + }) + }, + + /** + * 创建数据 + * @param params 数据参数 + * @returns 执行结果 + */ + save(params: Record) { + return request.post({ + url: '/dice/reward_config_record/DiceRewardConfigRecord/save', + data: params + }) + }, + + /** + * 更新数据 + * @param params 数据参数 + * @returns 执行结果 + */ + update(params: Record) { + return request.put({ + url: '/dice/reward_config_record/DiceRewardConfigRecord/update', + data: params + }) + }, + + /** + * 删除数据 + * @param id 数据ID + * @returns 执行结果 + */ + delete(params: Record) { + return request.del({ + url: '/dice/reward_config_record/DiceRewardConfigRecord/destroy', + data: params + }) + }, + + /** + * 导入:将测试记录的权重写入 DiceRewardConfig 与 DiceLotteryPoolConfig,并刷新缓存 + * @param record_id 测试记录 ID + * @param lottery_config_id 可选,导入档位权重到的奖池配置 ID,不传则用记录内的 lottery_config_id + */ + importFromRecord(params: { record_id: number; lottery_config_id?: number | null }) { + return request.post({ + url: '/dice/reward_config_record/DiceRewardConfigRecord/importFromRecord', + data: params + }) + } +} diff --git a/saiadmin-artd/src/views/plugin/dice/reward_config/index/index.vue b/saiadmin-artd/src/views/plugin/dice/reward_config/index/index.vue index ec84e09..64f88ad 100644 --- a/saiadmin-artd/src/views/plugin/dice/reward_config/index/index.vue +++ b/saiadmin-artd/src/views/plugin/dice/reward_config/index/index.vue @@ -16,6 +16,14 @@ > T1-T5 与 BIGWIN 权重配比 + + 测试中奖 + @@ -60,6 +68,8 @@ /> + + @@ -70,8 +80,10 @@ import TableSearch from './modules/table-search.vue' import EditDialog from './modules/edit-dialog.vue' import WeightRatioDialog from './modules/weight-ratio-dialog.vue' + import WeightTestDialog from './modules/weight-test-dialog.vue' const weightRatioVisible = ref(false) + const weightTestVisible = ref(false) // 搜索表单 const searchForm = ref>({ diff --git a/saiadmin-artd/src/views/plugin/dice/reward_config/index/modules/weight-test-dialog.vue b/saiadmin-artd/src/views/plugin/dice/reward_config/index/modules/weight-test-dialog.vue new file mode 100644 index 0000000..08f0ff6 --- /dev/null +++ b/saiadmin-artd/src/views/plugin/dice/reward_config/index/modules/weight-test-dialog.vue @@ -0,0 +1,227 @@ + + + + + diff --git a/saiadmin-artd/src/views/plugin/dice/reward_config_record/index/index.vue b/saiadmin-artd/src/views/plugin/dice/reward_config_record/index/index.vue new file mode 100644 index 0000000..0305f2c --- /dev/null +++ b/saiadmin-artd/src/views/plugin/dice/reward_config_record/index/index.vue @@ -0,0 +1,217 @@ + + + diff --git a/saiadmin-artd/src/views/plugin/dice/reward_config_record/index/modules/detail-drawer.vue b/saiadmin-artd/src/views/plugin/dice/reward_config_record/index/modules/detail-drawer.vue new file mode 100644 index 0000000..362c27f --- /dev/null +++ b/saiadmin-artd/src/views/plugin/dice/reward_config_record/index/modules/detail-drawer.vue @@ -0,0 +1,323 @@ + + + + + diff --git a/saiadmin-artd/src/views/plugin/dice/reward_config_record/index/modules/edit-dialog.vue b/saiadmin-artd/src/views/plugin/dice/reward_config_record/index/modules/edit-dialog.vue new file mode 100644 index 0000000..ca92668 --- /dev/null +++ b/saiadmin-artd/src/views/plugin/dice/reward_config_record/index/modules/edit-dialog.vue @@ -0,0 +1,150 @@ + + + diff --git a/saiadmin-artd/src/views/plugin/dice/reward_config_record/index/modules/table-search.vue b/saiadmin-artd/src/views/plugin/dice/reward_config_record/index/modules/table-search.vue new file mode 100644 index 0000000..d1d8c79 --- /dev/null +++ b/saiadmin-artd/src/views/plugin/dice/reward_config_record/index/modules/table-search.vue @@ -0,0 +1,62 @@ + + + diff --git a/server/app/dice/controller/lottery_pool_config/DiceLotteryPoolConfigController.php b/server/app/dice/controller/lottery_pool_config/DiceLotteryPoolConfigController.php index 8578a90..619ad42 100644 --- a/server/app/dice/controller/lottery_pool_config/DiceLotteryPoolConfigController.php +++ b/server/app/dice/controller/lottery_pool_config/DiceLotteryPoolConfigController.php @@ -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); } diff --git a/server/app/dice/controller/reward_config/DiceRewardConfigController.php b/server/app/dice/controller/reward_config/DiceRewardConfigController.php index ba25825..0a136df 100644 --- a/server/app/dice/controller/reward_config/DiceRewardConfigController.php +++ b/server/app/dice/controller/reward_config/DiceRewardConfigController.php @@ -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()); + } + } } diff --git a/server/app/dice/controller/reward_config_record/DiceRewardConfigRecordController.php b/server/app/dice/controller/reward_config_record/DiceRewardConfigRecordController.php new file mode 100644 index 0000000..977f5df --- /dev/null +++ b/server/app/dice/controller/reward_config_record/DiceRewardConfigRecordController.php @@ -0,0 +1,168 @@ +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()); + } + } +} diff --git a/server/app/dice/logic/reward_config/DiceRewardConfigLogic.php b/server/app/dice/logic/reward_config/DiceRewardConfigLogic.php index 5d1a072..2c74a8d 100644 --- a/server/app/dice/logic/reward_config/DiceRewardConfigLogic.php +++ b/server/app/dice/logic/reward_config/DiceRewardConfigLogic.php @@ -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 奖池配置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]; + } } diff --git a/server/app/dice/logic/reward_config_record/DiceRewardConfigRecordLogic.php b/server/app/dice/logic/reward_config_record/DiceRewardConfigRecordLogic.php new file mode 100644 index 0000000..2343950 --- /dev/null +++ b/server/app/dice/logic/reward_config_record/DiceRewardConfigRecordLogic.php @@ -0,0 +1,125 @@ +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 + */ + 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 → DiceRewardConfig,tier_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(); + } +} diff --git a/server/app/dice/model/reward_config/DiceRewardConfigRecord.php b/server/app/dice/model/reward_config/DiceRewardConfigRecord.php new file mode 100644 index 0000000..2f48475 --- /dev/null +++ b/server/app/dice/model/reward_config/DiceRewardConfigRecord.php @@ -0,0 +1,32 @@ +出现次数 + * @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; +} diff --git a/server/app/dice/model/reward_config_record/DiceRewardConfigRecord.php b/server/app/dice/model/reward_config_record/DiceRewardConfigRecord.php new file mode 100644 index 0000000..50ad3ac --- /dev/null +++ b/server/app/dice/model/reward_config_record/DiceRewardConfigRecord.php @@ -0,0 +1,34 @@ +出现次数 + * @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; +} diff --git a/server/app/dice/validate/reward_config_record/DiceRewardConfigRecordValidate.php b/server/app/dice/validate/reward_config_record/DiceRewardConfigRecordValidate.php new file mode 100644 index 0000000..2a7ef74 --- /dev/null +++ b/server/app/dice/validate/reward_config_record/DiceRewardConfigRecordValidate.php @@ -0,0 +1,42 @@ + 'require', + ]; + + /** + * 定义错误信息 + */ + protected $message = [ + 'test_count' => '测试次数:100/500/1000必须填写', + ]; + + /** + * 定义场景 + */ + protected $scene = [ + 'save' => [ + 'test_count', + ], + 'update' => [ + 'test_count', + ], + ]; + +} diff --git a/server/plugin/saiadmin/config/route.php b/server/plugin/saiadmin/config/route.php index e5b73ed..766df8a 100644 --- a/server/plugin/saiadmin/config/route.php +++ b/server/plugin/saiadmin/config/route.php @@ -105,10 +105,13 @@ Route::group('/core', function () { fastRoute('dice/reward_config/DiceRewardConfig', \app\dice\controller\reward_config\DiceRewardConfigController::class); Route::get('/dice/reward_config/DiceRewardConfig/weightRatioList', [\app\dice\controller\reward_config\DiceRewardConfigController::class, 'weightRatioList']); Route::post('/dice/reward_config/DiceRewardConfig/batchUpdateWeights', [\app\dice\controller\reward_config\DiceRewardConfigController::class, 'batchUpdateWeights']); + Route::post('/dice/reward_config/DiceRewardConfig/runWeightTest', [\app\dice\controller\reward_config\DiceRewardConfigController::class, 'runWeightTest']); fastRoute('dice/lottery_pool_config/DiceLotteryPoolConfig', \app\dice\controller\lottery_pool_config\DiceLotteryPoolConfigController::class); Route::get('/dice/lottery_pool_config/DiceLotteryPoolConfig/getOptions', [\app\dice\controller\lottery_pool_config\DiceLotteryPoolConfigController::class, 'getOptions']); Route::get('/dice/lottery_pool_config/DiceLotteryPoolConfig/getCurrentPool', [\app\dice\controller\lottery_pool_config\DiceLotteryPoolConfigController::class, 'getCurrentPool']); Route::post('/dice/lottery_pool_config/DiceLotteryPoolConfig/updateCurrentPool', [\app\dice\controller\lottery_pool_config\DiceLotteryPoolConfigController::class, 'updateCurrentPool']); + fastRoute('dice/reward_config_record/DiceRewardConfigRecord', \app\dice\controller\reward_config_record\DiceRewardConfigRecordController::class); + Route::post('/dice/reward_config_record/DiceRewardConfigRecord/importFromRecord', [\app\dice\controller\reward_config_record\DiceRewardConfigRecordController::class, 'importFromRecord']); // 数据表维护 Route::get("/database/index", [\plugin\saiadmin\app\controller\system\DataBaseController::class, 'index']);