文档
This commit is contained in:
39
server/db/README_WEIGHT_TEST.md
Normal file
39
server/db/README_WEIGHT_TEST.md
Normal file
@@ -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` 为测试记录表增加字段(逐条执行,已存在的列可忽略)。
|
||||
21
server/docs/DICE_REWARD_CONFIG_START_INDEX_IMPACT.md
Normal file
21
server/docs/DICE_REWARD_CONFIG_START_INDEX_IMPACT.md
Normal file
@@ -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` 已正确写入。
|
||||
192
server/docs/PERFORMANCE_AND_QPS_ANALYSIS.md
Normal file
192
server/docs/PERFORMANCE_AND_QPS_ANALYSIS.md
Normal file
@@ -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 |
|
||||
|
||||
---
|
||||
|
||||
## 雅????恙???d????黎????
|
||||
### 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 ????g?嶸?
|
||||
- **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% 訝뷴?餓???o??답???? QPS 鴉?? `playStart` ?얏?竊?*瀯쇔????瀛?250節?00 QPS** ???訝뷴?????담??
|
||||
若??????ab/wrk/k6 嶺?? `playStart` ??말誤???e?役??瀯?? MySQL ?€?瑥???edis ?썰릎??????ζ?嶺??轝→????????
|
||||
---
|
||||
|
||||
## 雅??????뻠溫????
|
||||
| 映삣? | 兩븃? |
|
||||
|------|------|
|
||||
| ??쉰 | ??벨溫양쉰 `CACHE_MODE=redis`竊?뭉瀯??鵝욜? Redis 鵝?맏 Cache 要긷? |
|
||||
| 瓦??黎?| ??? `DB_POOL_MAX`竊?? 32節?4竊??岳?? ??Worker ?곤?亮띄???MySQL max_connections |
|
||||
| 餓g? | `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. **??????**
|
||||
- 佯??竊??訝???g?瑥룡?????뭄?? P99 ?????xx 訝???뜻??겹??
|
||||
- MySQL竊???ζ?????θ???nnoDB 烏??嶺????
|
||||
- Redis竊???ζ????訝?????餓ㅸ?????
|
||||
3. **?띌??ㅶ?**
|
||||
- ??QPS 訝????CPU ???譯???鴉?????餓g?????뷴???
|
||||
- ??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 餓g?掠????? 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` ?????訝??瑥????? | 堊요?????<3F>?黎??凉?만役????|
|
||||
| 訝?| 凉???MySQL ?€?瑥€?恙??倻?>200ms竊?| 若??????뇨凉??鴉????SQL??|
|
||||
| 鵝?| ???孃????? QPS 訝?P99 | ??ab/wrk/k6 野?`playStart` ???竊?빳?경?若??????⒴??뜻???|
|
||||
|
||||
### 7.5 窯?????竊??謠??쇽?
|
||||
|
||||
- ??쉰掠?+ 榮℡?竊???δ????訝븀?窯????〃訝??壤???f??얍?恙???답??????벧 **20%節?0%** ??? QPS??- ?삥? playStart ??2 轝▼?鵝??佯?????黎?? 2 轝?round-trip竊?*playStart ???黎????瀛?? 5%節?5%**??- ev ?밥맏 Redis 榮??竊??亮뜹?訝?**playStart** 烏??塋??訝??竊????playStart QPS ????????**10%節?0%**竊??亮뜹?佯????竊???
|
||||
---
|
||||
|
||||
**瑥닸?**竊??瓦?QPS 訝??????맏?뷰?餓g?訝??營??鴉곁?竊?????㎬?餓ε?役??瀛요????訝뷴???뻠溫??窯??/??벨??????弱??饔?`playStart` 訝??壤????????녔??g??????
|
||||
49
server/docs/PLAY_START_FLOW_COMPARISON.md
Normal file
49
server/docs/PLAY_START_FLOW_COMPARISON.md
Normal file
@@ -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),我可以按上述思路给出具体修改方案(含要改的类/方法名和伪代码)。
|
||||
78
server/docs/ROLL_NUMBER_ANALYSIS.md
Normal file
78
server/docs/ROLL_NUMBER_ANALYSIS.md
Normal file
@@ -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 在路径组内的权重或条数占比。
|
||||
Reference in New Issue
Block a user