From 72b43759f1aec04fffba4bbc593853cdccf8a7f5 Mon Sep 17 00:00:00 2001 From: zhenhui <1276357500@qq.com> Date: Mon, 16 Mar 2026 09:10:39 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/db/README_WEIGHT_TEST.md | 39 ++++ .../DICE_REWARD_CONFIG_START_INDEX_IMPACT.md | 21 ++ server/docs/PERFORMANCE_AND_QPS_ANALYSIS.md | 192 ++++++++++++++++++ server/docs/PLAY_START_FLOW_COMPARISON.md | 49 +++++ server/docs/ROLL_NUMBER_ANALYSIS.md | 78 +++++++ 5 files changed, 379 insertions(+) create mode 100644 server/db/README_WEIGHT_TEST.md create mode 100644 server/docs/DICE_REWARD_CONFIG_START_INDEX_IMPACT.md create mode 100644 server/docs/PERFORMANCE_AND_QPS_ANALYSIS.md create mode 100644 server/docs/PLAY_START_FLOW_COMPARISON.md create mode 100644 server/docs/ROLL_NUMBER_ANALYSIS.md diff --git a/server/db/README_WEIGHT_TEST.md b/server/db/README_WEIGHT_TEST.md new file mode 100644 index 0000000..b2543ca --- /dev/null +++ b/server/db/README_WEIGHT_TEST.md @@ -0,0 +1,39 @@ +# 一键测试权重 - 数据库 SQL 操作说明 + +## 1. 新建表 dice_play_record_test + +测试用游玩记录表,结构与 `dice_play_record` 完全一致,不关联真实玩家(`player_id` 填 0),用于写入模拟数据并可一键清空。 + +**执行脚本:** `dice_play_record_test.sql` + +```sql +-- 若表已存在可跳过;执行前请确认 dice_play_record 表已存在 +CREATE TABLE IF NOT EXISTS `dice_play_record_test` LIKE `dice_play_record`; +``` + +## 2. 扩展表 dice_reward_config_record + +为一键测试权重增加进度与结果字段:总次数、已完成次数、状态、备注、顺/逆时针次数、档位出现次数(档位概率)。 + +**执行脚本:** `dice_reward_config_record_add_test_progress.sql` + +若某列已存在会报错,可跳过该条继续执行下一条。 + +- `total_play_count`:总模拟次数(s_count + n_count) +- `over_play_count`:已完成次数,每完成 10 条写入 `dice_play_record_test` 后更新 +- `status`:-1 失败,0 进行中,1 成功 +- `remark`:失败时记录原因 +- `s_count`:顺时针模拟次数 +- `n_count`:逆时针模拟次数 +- `tier_counts`:档位出现次数 JSON(T1=>count),用于档位概率 + +原有字段 `result_counts` 已存在,用于点数出现次数(点数概率)。 + +## 3. 导入操作 + +`dice_reward_config_record` 的**导入**功能保持不变:可将测试记录的权重快照导入到 `DiceReward` 与 `DiceLotteryPoolConfig`,并刷新缓存。无需额外 SQL。 + +## 执行顺序建议 + +1. 先执行 `dice_play_record_test.sql` 创建测试表。 +2. 再执行 `dice_reward_config_record_add_test_progress.sql` 为测试记录表增加字段(逐条执行,已存在的列可忽略)。 diff --git a/server/docs/DICE_REWARD_CONFIG_START_INDEX_IMPACT.md b/server/docs/DICE_REWARD_CONFIG_START_INDEX_IMPACT.md new file mode 100644 index 0000000..7d31704 --- /dev/null +++ b/server/docs/DICE_REWARD_CONFIG_START_INDEX_IMPACT.md @@ -0,0 +1,21 @@ +# 移除 DiceRewardConfig 表 s_start_index / n_start_index 的说明(已处理) + +**`s_start_index`**(顺时针起始索引)、**`n_start_index`**(逆时针起始索引)已从业务与表单中移除,起始索引统一使用 **dice_reward.start_index**。 + +## 1. 当前状态(移除后无影响) + +| 位置 | 处理情况 | +|------|----------| +| **PlayStartLogic** | 已使用 **DiceReward** 的 `start_index`,不读 config,无影响 | +| **DiceRewardLogic::getListWithConfig** | 已去掉对 config 的 join 与回填,仅使用 `r.start_index` | +| **DiceRewardConfigValidate** | 已从规则与 save/update 场景中删除这两项 | +| **奖励配置编辑弹窗** | 已移除「顺时针/逆时针起始索引」表单项及提交字段 | +| **DiceRewardConfig 模型** | 已从属性注释中删除 | + +## 2. 数据库表结构 + +若表中仍有这两列,可执行: + +- `server/db/dice_reward_config_drop_start_index.sql`:删除 `s_start_index`、`n_start_index` 列。 + +执行前请确认已通过「创建奖励对照」生成 dice_reward 数据,且 `dice_reward.start_index` 已正确写入。 diff --git a/server/docs/PERFORMANCE_AND_QPS_ANALYSIS.md b/server/docs/PERFORMANCE_AND_QPS_ANALYSIS.md new file mode 100644 index 0000000..c92911c --- /dev/null +++ b/server/docs/PERFORMANCE_AND_QPS_ANALYSIS.md @@ -0,0 +1,192 @@ +# 游????????QPS ????? + +## ????????????? +| 项? | 说? | +|------|------| +| ????? | Webman (?? Workerman)?驻??| +| HTTP ??? | `http://0.0.0.0:6688` | +| Worker ?? | `cpu_count() * 4`?? 8 ??= 32 ???? | +| ???| MySQL?hinkORM???? max=20/min=2 | +| ?? | Redis?hink-cache ?? `redis`?????max=20/min=2 | +| ??驱?注? | `config/cache.php` ?? `file`????使?该?????走?件?`config/think-cache.php` ?? `redis`?PI ??/???????Redis | + +--- + +## ??????????????? +### 2.1 ???????? +| ?? | ???| 主???? | ???????????? | +|------|------|----------|------------------------| +| `POST /api/game/playStart` | ????游????? | ?? DB + Redis + ?? | ?????| +| `POST /api/game/buyLotteryTickets` | ?????| ?????表???+ Redis ?? | ?| +| `GET /api/game/config` | 游??? | ?表 DiceConfig ?????? | ??| +| `GET /api/game/lotteryPool` | ???? | DiceRewardConfig ?? | ?| +| `POST /api/v1/getGameUrl` | ??游?????? | ???? + JWT + Redis | ?| +| `POST /api/v1/getPlayerInfo` | ??信? | ??username ??DicePlayer | ??建? username ????索??| +| `POST /api/v1/getPlayerGameRecord` | 游?记??? | ?? + ??? N+1 ???| ?| +| `POST /api/v1/setPlayerWallet` | ?????? | ???????+ ????| ?| + +### 2.2 `playStart` ???请?????????????? +???游??次??????? +**????** + +1. `DicePlayer::find($playerId)` ?????????2. `DiceRewardConfig::getCachedMinRealEv()` ?????????并??? +3. `LotteryService::getOrCreate()` ??????Redis ??`DicePlayer::find` + `DiceLotteryPoolConfig::where('type',0/1)->find()` 两? +4. `DiceLotteryPoolConfig::find($configId)` ??????ID ????5. **????*? + - `DicePlayRecord::create` + - `DicePlayer::find($playerId)`??次???????? + - `DicePlayer::save` + - ??? `DicePlayerTicketRecord::create`?5 ??? + - `DiceLotteryPoolConfig::where('id')->update(['ev' => Db::raw(...)])` ????? + - `DicePlayerWalletRecord::create` +6. ?????`DicePlayer::find($playerId)`??UserCache::setUser` + +**Redis?* + +- `DiceRewardConfig::getCachedInstance()`?? getCachedByTier ?????????????- `LotteryService::getOrCreate()` ??????家????- `UserCache::setUser()` ??????信???? +??????好????????? `playStart` ???**6? ?DB 访?**??????2 ?find ???? ?update 彩????? ?create??? +?????中???????????????????家???次??? +--- + +## ??????????? + +### 3.1 ??? +1. **???? Worker ??* + - ??? DB ???max=20?orker ??= CPU?4??? 32 ?Worker ????????? 20 ????????????? + - 建???????? `DB_POOL_MAX`?? 32?4??并???MySQL `max_connections` ?????? +2. **`playStart` ???????** + - ??? `DicePlayer::find($playerId)`??????`DicePlayer::find($playerId)`?? + - ???为??????????已??`$player` ???????段??????SELECT?? +3. **`DiceLotteryPoolConfig::where('id', $configId)->update(['ev' => Db::raw(...)])`** + - ??????????? `ev`??并??????????????为???? + - ???????步累???????/次???????使? Redis 计??????步? DB?? +4. **索?** + - `dice_player.username`??dice_play_record.player_id`??dice_play_record.create_time`?? + `dice_player_wallet_record.player_id`??dice_player_ticket_record.player_id` ????索???表????????? + - 建????username` ???索??player_id` + `create_time` ??索????????件????? +### 3.2 ?? + +1. **??驱?????* + - `config/cache.php` ?? `file`???????该??? Cache??走?件?QPS ?? I/O ?????为???? + - 建???产??使? Redis??? `CACHE_MODE=redis` ?think-cache ??default ???? +2. **??????** + - `DiceRewardConfig::getCachedInstance()` 已???????????+ Redis???????好?? + - ?????????????`refreshCache()`???????????? +3. **UserCache ???* + - ?? `getUser`/`setUser` ??AES ?????QPS ?CPU ?????????????????????? CPU ??????????????value?? +### 3.3 ???? N+1 + +- `getPlayerGameRecord`??????`DicePlayRecord`?? `whereIn('id', $playerIds)` ??家并???? + ????已?????家????? N+1?????limit ??????????????????limit ?????100 ????? +- `getPlayerWalletRecord` / `getPlayerTicketRecord` 使? `with(['dicePlayer'])` ??载?????? +### 3.4 ?????? +- Worker ??= CPU?4 ??????????1 ?DB ?????????0 ??????满?? +- 建?????????`DB_POOL_WAIT_TIMEOUT` ?????????????????????????? +--- + +## ????PS ?????????????? +### 4.1 ????件 + +- 8 ??CPU?2 ?Worker??- MySQL/Redis ??????延????????????- ???中??????????otteryService??serCache ??中??? +### 4.2 ?????? +- **playStart**??次约 50?50 ms???????DB/Redis????Worker ?7?0 QPS?2 Worker ?**220?40 QPS**?? + ????DB ???? `dice_lottery_config.ev` ??????????影??*??估???? 200?00 QPS**??- **buyLotteryTickets**??次约 20?0 ms???约 **500?000+ QPS**????????满为??????- **getGameUrl / 平???**?????? DB ????次约 30?0 ms???约 **400?00 QPS**?????走????? +### 4.3 混??? + +??70% ?`playStart`??0% 为?????????? QPS ?? `playStart` ???*综?????250?00 QPS** ???为??????? +??????ab/wrk/k6 ?? `playStart` ?????????? MySQL ?????edis ?中?????????次???????? +--- + +## ?????????? +| 类? | 建? | +|------|------| +| ?? | ??设置 `CACHE_MODE=redis`???使? Redis ? Cache 驱? | +| ???| ??? `DB_POOL_MAX`?? 32?4???? ??Worker ??并???MySQL max_connections | +| 代? | `playStart` ??????已?? `$player`??????`DicePlayer::find($playerId)` | +| ???| ?`username`??player_id`??create_time` ????询?段?索???????| +| ????? | `dice_lottery_config.ev` ???????为 Redis ?? + ??/????? DB????????| +| ??? | ?`playStart`??buyLotteryTickets`??setPlayerWallet` ???????????????MySQL ????????????| +| ??? | 使? ab/wrk/k6 ?`/api/game/playStart` ?????????????P99 延????QPS | + +--- + +## ??????????QPS ?P99 + +1. **??? playStart** + - 使???? JWT????????? token???`POST /api/game/playStart` ??body `direction=0` ??`1`?? + - 示????? URL ??token?? + ```bash + # 使? ab + ab -n 1000 -c 32 -p post_body.json -T application/json -H "token: YOUR_JWT" http://127.0.0.1:6688/api/game/playStart + + # 使? wrk?? lua ????token ??body? wrk -t4 -c32 -d30s -s play_start.lua http://127.0.0.1:6688/api/game/playStart + ``` + - ?????? Requests per second?PS?? Time per request???? P99 ?工??????? +2. **??????** + - ????????请??????? P99 ?????xx ??????? + - MySQL???????????nnoDB ?????? + - Redis????????????令????? +3. **????** + - ??QPS ????CPU ???????????代???????? + - ??DB ??????????? ???????????????????????? + - ??Redis 延??? ???大 key????令?????????? +--- + +## ????????????????? + +??*???产??*???????????????项????????QPS ????? +### 7.1 ??????????????????? + +| ???| ??? | 说? | +|--------|------|------| +| ?| ?????设置 `CACHE_MODE=redis` | ??走?件?????? I/O ?????? think-cache ??????| +| ?| ??? DB ????`DB_POOL_MAX=32` ??`64` | ?? ??Worker ???32?????????????????| +| ?| ??? Redis ????`REDIS_POOL_MAX=32` | ?Worker ???????? Redis ????????| +| ?| ?? MySQL `max_connections` ????????? | ????署????? ? DB_POOL_MAX ???MySQL ????| + +**示? .env ????* + +```env +CACHE_MODE=redis +DB_POOL_MAX=32 +DB_POOL_MIN=4 +REDIS_POOL_MAX=32 +``` + +### 7.2 ??????索??????? + +| ???| ??? | 说? | +|--------|------|------| +| ?| ?`dice_player.username` 建??索? | ????etPlayerInfo??oken ??件???username ???????表?????| +| ?| ?`dice_play_record(player_id, create_time)` 建????| 游?记??????计???+?????????页?????| +| ?| 为?水表 `player_id`??create_time` 建索?| ?`dice_player_wallet_record`??dice_player_ticket_record` ???家????????????| + +**示? SQL????表?????** + +```sql +-- ???????添? +ALTER TABLE dice_player ADD UNIQUE INDEX uk_username (username); +ALTER TABLE dice_play_record ADD INDEX idx_player_create (player_id, create_time); +ALTER TABLE dice_player_wallet_record ADD INDEX idx_player_create (player_id, create_time); +ALTER TABLE dice_player_ticket_record ADD INDEX idx_player_create (player_id, create_time); +``` + +### 7.3 代?????? DB ??????? +| ???| ??? | 说? | +|--------|------|------| +| ?| `playStart` ??????已?? `$player`????`DicePlayer::find($playerId)` | ?1 ?SELECT??????? `$player` ???????? 1 ?SELECT??| +| ?| `game/config` ???DiceConfig ????| ???????????Redis ?? 1? ???????表????| +| ?| `dice_lottery_config.ev` ?为 Redis ?? + ????? DB | ???? `UPDATE dice_lottery_config SET ev=ev-?`?????????????????N ?????次??| + +### 7.4 ??????? +| ???| ??? | 说? | +|--------|------|------| +| ?| ?`playStart`??buyLotteryTickets` ???????????? | 便?????????????| +| ?| ???MySQL ??询????>200ms?| ????????????SQL??| +| ?| ???????? QPS ?P99 | ??ab/wrk/k6 ?`playStart` ?????????????????| + +### 7.5 ?????????? + +- ???+ 索????????为?????????????????????? **20%?0%** ??? QPS??- ?? playStart ??2 次?????????? 2 ?round-trip?*playStart ????????? 5%?5%**??- ev ?为 Redis ????并??**playStart** ??????????playStart QPS ????????**10%?0%**??并???????? +--- + +**说?**???QPS ????????代?????估???????以???线????为???????/???????????`playStart` ?????????????????? diff --git a/server/docs/PLAY_START_FLOW_COMPARISON.md b/server/docs/PLAY_START_FLOW_COMPARISON.md new file mode 100644 index 0000000..f01ccc0 --- /dev/null +++ b/server/docs/PLAY_START_FLOW_COMPARISON.md @@ -0,0 +1,49 @@ +# 抽奖流程对比:当前实现 vs 预期流程 + +## 你描述的预期流程 + +1. **先判断中的是 T1–T5 中的哪个奖** + → 按彩金池/玩家权重抽档位 T1–T5。 + +2. **根据 (1) 中奖类型从 DiceRewardConfig 读取数据,再根据权重 weight 抽取点数 grid_number** + → 该档位下多条配置,按每条配置的 **weight** 抽一条,得到这条配置的 **grid_number**(以及 real_ev 等)。 + +3. **根据抽取的 grid_number 查找有无对应的 s_end_index(n_end_index)** + → 用上一步得到的 grid_number(及方向)去查「带该 grid_number 且带 s_end_index/n_end_index 的配置」是否存在。 + +4. **若有则输出 s_end_index(n_end_index)对应的点数和起始点数;中奖数据仍用步骤 2 抽到的 grid_number 对应配置** + → 输出:起始点、终点(s_end_index 或 n_end_index)、点数和 roll_number;中奖金额等用步骤 2 抽到的那条配置。 + +5. **判断是否中大奖** + → 若点数和为豹子组合 (5,10,15,20,25,30),其中 5 和 30 必中大奖,其余按 BIGWIN 的 weight 再判一次;中大奖则返回 BIGWIN 的 roll_array。 + +--- + +## 当前实现 + +| 步骤 | 预期 | 当前实现 | 是否一致 | +|------|------|----------|----------| +| 1 | 先抽 T1–T5 档位 | 按彩金池/玩家权重抽 T1–T5 | ✅ 一致 | +| 2 | 按该档位配置的 **weight** 抽取 **grid_number**(即抽一条配置) | 该档位配置**等权随机**选一条 `chosen`,**没有用 weight**;且当前用的 **grid_number 来自后面的「路径」配置**,不是来自这条 chosen | ❌ 不一致:未按 weight 抽,grid_number 来源也不对 | +| 3 | 根据 grid_number 查 s_end_index / n_end_index | 根据 **chosen.id** 查「s_end_index = chosen.id 或 n_end_index = chosen.id」的配置,得到若干 **startCandidates**(路径列表) | ❌ 不一致:是按「终点 id」查路径,不是按 grid_number 查 | +| 4 | 若有则输出终点、点数和、起始点;中奖数据用步骤 2 的配置 | 从 startCandidates 里再**等权随机**一条 `startRecord`,用 **startRecord.id** 作起始、**startRecord.grid_number** 作点数和、**startRecord.s_end_index/n_end_index** 作终点;中奖数据(real_ev 等)用的是 **chosen** | ⚠️ 部分一致:终点、起始、点数和都有,但 grid_number 来自路径而不是「按 weight 抽出的那条配置」 | +| 5 | 豹子点数 5,10,15,20,25,30;5/30 必中大奖;其余按 BIGWIN.weight 判 | 逻辑一致:5/30 必中大奖,其余用 BIGWIN 的 weight 判定 | ✅ 一致 | + +### weight 是否实例化(入缓存) + +- **BIGWIN**:缓存里有完整行,含 **weight**,`getCachedByTierAndGridNumber('BIGWIN', rollNumber)` 返回的配置里带 weight,已用于步骤 5。✅ 已实例化。 +- **T1–T5**:`getCachedByTier(tier)` 返回的每条配置也是完整行(含 weight),但当前代码**没有用这些 weight**,只用 `array_rand` 等权选一条。即:weight 已在缓存里,但**未参与抽奖**。⚠️ 已实例化但未使用。 + +--- + +## 结论与建议 + +- **不一致点**: + - 步骤 2:应用「该档位下按 **weight** 抽一条配置」,用这条配置的 **grid_number**(和 real_ev 等);当前是等权抽一条且 grid_number 实际来自路径。 + - 步骤 3:应用「用步骤 2 得到的 **grid_number**(及方向)查是否有 s_end_index/n_end_index」;当前是用 chosen.id 查「以该 id 为终点的路径」。 + +- **建议**: + - 改为「先按档位内 weight 抽一条配置」,以该条为**唯一**来源得到 grid_number、real_ev、以及 s_end_index/n_end_index(若表结构是一条配置同时带 grid_number 与 s_end_index/n_end_index)。 + - 若表结构是「奖励配置」与「路径配置」分离,则需在步骤 2 按 weight 抽到 grid_number 后,再按 **grid_number + 方向** 查路径表得到 s_end_index/n_end_index 与起始点;并保证只对「在该方向下有有效 s_end_index/n_end_index 的配置」做 weight 抽取。 + +若你确认表结构(是否同一张表、是否一条既有 grid_number 又有 s_end_index/n_end_index),我可以按上述思路给出具体修改方案(含要改的类/方法名和伪代码)。 diff --git a/server/docs/ROLL_NUMBER_ANALYSIS.md b/server/docs/ROLL_NUMBER_ANALYSIS.md new file mode 100644 index 0000000..d43e5c8 --- /dev/null +++ b/server/docs/ROLL_NUMBER_ANALYSIS.md @@ -0,0 +1,78 @@ +# 色子点数(5–30)抽中概率分析 + +## 一、当前流程(为何 5 和 30 容易出、部分点数可能摇不到) + +点数 `rollNumber`(5–30)由**两步**决定,而不是在 5–30 里直接按权重抽一次: + +1. **先抽档位** + 按池子/玩家权重抽 T1–T5 之一。 + +2. **再在该档位内按权重抽一条“奖励配置”** + `$chosen = drawRewardByWeight(tierRewards)`,得到一条配置,记其主键为 `chosenId`。 + +3. **按 chosenId 取“路径候选”** + - 顺时针:`startCandidates = getCachedBySEndIndex(chosenId)` → 所有 **s_end_index = chosenId** 的配置。 + - 逆时针:`startCandidates = getCachedByNEndIndex(chosenId)` → 所有 **n_end_index = chosenId** 的配置。 + +4. **在路径候选中再按权重抽一条** + `$startRecord = drawRewardByWeight(startCandidates)`,最终 **rollNumber = $startRecord['grid_number']**。 + +因此: + +- **本局能出现哪些点数,完全由“当前 chosenId 对应的路径组”决定。** +- 只有**在 startCandidates 里出现的 grid_number** 才会被摇到;**不在该组里的点数本局根本不会参与抽取**,相当于被“跳过”。 + +## 二、为何 5 和 30 摇到的概率会很大 + +可能原因: + +1. **路径数据里 5、30 占比高** + 对很多 `chosenId`,其 `s_end_index=chosenId`(或 n_end_index)的配置里,grid_number=5 和 30 的条数多、或 weight 设得大,其它点数少/权重小,所以在这组里一抽就经常是 5 或 30。 + +2. **被抽中的 chosenId 偏集中在“含 5、30 的路径组”** + 档位内按权重抽到的 `$chosen` 的 id,如果经常落在某几类 id 上,而这些 id 对应的路径组里又以 5、30 为主,整体就会表现为 5、30 出现很多。 + +3. **5、30 出现在很多路径组里** + 若 5、30 对应的配置的 `s_end_index`/`n_end_index` 覆盖了很多不同的 id(即出现在很多“路径组”里),而其它点数只出现在少数几个 id 的路径组里,那么 5、30 被抽到的机会自然更多。 + +## 三、为何有些点数“摇不到、像被跳过” + +- 某点数 **grid_number = G** 本局能出,**仅当**: + 当前 `chosenId` 对应的路径组里,**存在至少一条配置的 grid_number = G**(且 weight>0 会参与权重抽)。 +- 若在**所有** `s_end_index = 某 id`(或 n_end_index)的路径组里,**都没有** grid_number=12 的配置,那么 12 就**永远不会**被摇到,即被“跳过”。 +- 若 12 只出现在“很少被选中的 chosenId”对应的路径组里(例如这些 id 在档位内权重很低),那 12 就会**很少**出现。 + +所以:**不是代码故意跳过某些点数,而是这些点数在当前数据下,没有进入“本局实际参与抽奖的那组路径”里。** +5、30 摇得多 = 它们在这组路径里权重大或出现次数多;某些点数摇不到 = 它们没进这组路径或权重为 0。 + +## 四、建议的数据检查(在库里直接查) + +在 `dice_reward_config` 表里可以做两类检查: + +**1)每个点数是否至少能出现在某个路径组里(避免永远摇不到)** + +- 顺时针:对每个 grid_number(5–30),是否存在至少一行 **s_end_index = 某个在 T1–T5 里出现过的 id**,且该行 weight > 0。 + (“在 T1–T5 里出现过的 id” = 作为某条 T1–T5 配置的主键 id。) +- 逆时针:同上,把 `s_end_index` 换成 `n_end_index`。 + +若某点数在顺时针(或逆时针)下没有任何一条这样的行,则该点数在该方向下**永远不会**被摇到。 + +**2)各点数在“路径组内”的权重是否过于悬殊** + +- 对常见的 chosenId(例如在 T1 里权重高的几条配置的 id),查: + `SELECT grid_number, SUM(weight) FROM dice_reward_config WHERE s_end_index = ? AND tier IN ('T1','T2',...) GROUP BY grid_number` + 看 5、30 的权重和是否明显高于 6–29,导致在该路径组内一抽就经常是 5 或 30。 + +## 五、可选:打开调试日志看“本局路径组里有哪些点数” + +在 `PlayStartLogic` 里,在 `$startRecord = drawRewardByWeight(...)` 之后可加一段**仅调试时启用**的日志,例如: + +- 打出:`chosenId`、`direction`、本局路径组里出现的 **grid_number 列表**及每个 grid_number 的**权重和**(或条数)。 +这样可以看到:每次抽奖时“实际参与抽奖的点数集合”是哪些,5、30 在该组里的权重是否偏大,以及哪些点数从未出现在日志里(即被跳过)。 + +--- + +**结论**: +- 5 和 30 摇到的概率大,是因为在“当前 chosenId 对应的路径组”里,它们权重高或出现次数多。 +- 某些点数摇不到,是因为它们没有出现在任何“本局会用到”的路径组里,或只出现在极少被选中的路径组里。 +要平衡概率,需要从**路径数据**入手:保证每个点数 5–30 至少出现在若干路径组中且 weight>0,并调低 5、30 在路径组内的权重或条数占比。