Compare commits
44 Commits
dd264b1e97
...
master-v3
| Author | SHA1 | Date | |
|---|---|---|---|
| 0d19e4b6c4 | |||
| b20a1d276f | |||
| 5e54859a30 | |||
| 226675c6bd | |||
| e7a4a47329 | |||
| 99949c4c3c | |||
| 6ef3663957 | |||
| f8969097d7 | |||
| 0cc81299f0 | |||
| 8a4a268526 | |||
| 16a59c28d4 | |||
| eb9ade6d16 | |||
| 8c6c122dc2 | |||
| dfb37dd33a | |||
| 5d316ef7d6 | |||
| 58a4b229a8 | |||
| 0c4da1540d | |||
| 1980ff4af0 | |||
| 5eb0ac24cd | |||
| 307a942b8e | |||
| 51105dd1e0 | |||
| 136c18e413 | |||
| 9fb98dee3f | |||
| 37b0ee134e | |||
| c0d5258aee | |||
| 13dacc8fdd | |||
| 79c84c198a | |||
| 3f97905ffa | |||
| d77e390fa3 | |||
| 906539995d | |||
| 18944f0d48 | |||
| 90abab14a3 | |||
| a4c8f623be | |||
| e0b303c5d4 | |||
| 77db2357ba | |||
| cde5a851e5 | |||
| 9a43e1d8f2 | |||
| 37c0035bfc | |||
| 5628af683f | |||
| a5e2f2fdf7 | |||
| 5cd8330c9e | |||
| 1b65e25f11 | |||
| 1f25280dfd | |||
| b089f302de |
1
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{}
|
||||||
41
API对接文档.md
@@ -19,6 +19,7 @@
|
|||||||
- **请求方法**:项目路由多数使用 `Route::any`,对接建议统一使用 **POST**(便于 body 传参);个别接口文档中标注了 GET 参数。
|
- **请求方法**:项目路由多数使用 `Route::any`,对接建议统一使用 **POST**(便于 body 传参);个别接口文档中标注了 GET 参数。
|
||||||
- **编码**:`UTF-8`
|
- **编码**:`UTF-8`
|
||||||
- **Content-Type**:建议 `application/x-www-form-urlencoded` 或 `application/json`(以平台实际实现为准)
|
- **Content-Type**:建议 `application/x-www-form-urlencoded` 或 `application/json`(以平台实际实现为准)
|
||||||
|
- **必带凭证**:所有 `/api/v1/*` 接口均需带 `api-key`(与服务端 `.env` 中 `API_KEY` 一致);除 `/api/v1/authToken` 外另需 `auth-token`。详见 §2.2。
|
||||||
|
|
||||||
### 1.3 统一返回结构
|
### 1.3 统一返回结构
|
||||||
|
|
||||||
@@ -49,10 +50,10 @@
|
|||||||
|
|
||||||
## 2. 鉴权与对接流程(平台侧 /api/v1)
|
## 2. 鉴权与对接流程(平台侧 /api/v1)
|
||||||
|
|
||||||
平台侧接口分两步:
|
平台侧接口需统一携带请求头 **`api-key`**(与服务端 `.env` 中 `API_KEY` 一致),业务接口另需 **`auth-token`**。
|
||||||
|
|
||||||
1. **获取 `auth-token`**
|
1. **获取 `auth-token`**(同时携带 `api-key`)
|
||||||
2. **携带 `auth-token` 调用 `/api/v1/*` 业务接口**
|
2. **携带 `api-key` + `auth-token` 调用 `/api/v1/*` 业务接口**
|
||||||
|
|
||||||
### 2.1 获取 auth-token
|
### 2.1 获取 auth-token
|
||||||
|
|
||||||
@@ -100,11 +101,22 @@ signature = md5(agent_id + secret + time)
|
|||||||
- 密钥错误/签名错误/时间戳无效:`code=403`
|
- 密钥错误/签名错误/时间戳无效:`code=403`
|
||||||
- 服务端未配置密钥或生成失败:`code=500`
|
- 服务端未配置密钥或生成失败:`code=500`
|
||||||
|
|
||||||
### 2.2 调用 v1 业务接口(携带 auth-token)
|
### 2.2 平台 api-key(所有 /api/v1/* 必填)
|
||||||
|
|
||||||
除 `/api/v1/authToken` 外,其余 `/api/v1/*` 接口需要在请求头携带:
|
- **取值**:与服务端环境变量 `API_KEY` 完全一致(部署在 `server/.env`)
|
||||||
|
- **适用范围**:所有 `/api/v1/*` 接口(含 `/api/v1/authToken` 与业务接口)
|
||||||
|
- **携带方式**(任选其一,按优先级读取,先命中即采用):
|
||||||
|
1. 请求头 `api-key: <API_KEY>`(**推荐**)
|
||||||
|
2. URL 查询参数 `api_key=<API_KEY>`(或 `api-key=<API_KEY>`)
|
||||||
|
3. body 表单/JSON 字段 `api_key`(或 `api-key`)
|
||||||
|
- **未携带或错误**:`401` / `403`
|
||||||
|
|
||||||
- `auth-token: <authtoken>`
|
### 2.3 调用 v1 业务接口(携带 auth-token)
|
||||||
|
|
||||||
|
除 `/api/v1/authToken` 外,其余 `/api/v1/*` 接口需要携带:
|
||||||
|
|
||||||
|
- `api-key: <与 API_KEY 一致>`(请求头 / query / body 任选其一,参见 2.2)
|
||||||
|
- `auth-token: <authtoken>`(仅支持请求头)
|
||||||
|
|
||||||
当 `auth-token` 过期或失效,返回 `code=402`,需要重新调用 `/api/v1/authToken` 获取新 token。
|
当 `auth-token` 过期或失效,返回 `code=402`,需要重新调用 `/api/v1/authToken` 获取新 token。
|
||||||
|
|
||||||
@@ -116,7 +128,7 @@ signature = md5(agent_id + secret + time)
|
|||||||
|
|
||||||
- **路径**:`/api/v1/getGameUrl`
|
- **路径**:`/api/v1/getGameUrl`
|
||||||
- **方法**:POST
|
- **方法**:POST
|
||||||
- **请求头**:`auth-token`
|
- **请求头**:`api-key`、`auth-token`
|
||||||
- **说明**:根据平台用户名创建/登录玩家并生成登录 JWT,返回可直接打开的游戏地址。
|
- **说明**:根据平台用户名创建/登录玩家并生成登录 JWT,返回可直接打开的游戏地址。
|
||||||
|
|
||||||
#### 请求参数(body)
|
#### 请求参数(body)
|
||||||
@@ -124,7 +136,6 @@ signature = md5(agent_id + secret + time)
|
|||||||
| 参数名 | 必填 | 类型 | 说明 |
|
| 参数名 | 必填 | 类型 | 说明 |
|
||||||
| --- | --- | --- | --- |
|
| --- | --- | --- | --- |
|
||||||
| username | 是 | string | 玩家唯一账号(平台侧用户名) |
|
| username | 是 | string | 玩家唯一账号(平台侧用户名) |
|
||||||
| password | 否 | string | 默认 `123456` |
|
|
||||||
| time | 否 | int/string | 默认当前时间戳 |
|
| time | 否 | int/string | 默认当前时间戳 |
|
||||||
| lang | 否 | string | `zh` / `en`,默认 `zh` |
|
| lang | 否 | string | `zh` / `en`,默认 `zh` |
|
||||||
|
|
||||||
@@ -144,7 +155,7 @@ signature = md5(agent_id + secret + time)
|
|||||||
|
|
||||||
- **路径**:`/api/v1/getPlayerInfo`
|
- **路径**:`/api/v1/getPlayerInfo`
|
||||||
- **方法**:POST
|
- **方法**:POST
|
||||||
- **请求头**:`auth-token`
|
- **请求头**:`api-key`、`auth-token`
|
||||||
|
|
||||||
#### 请求参数
|
#### 请求参数
|
||||||
|
|
||||||
@@ -160,7 +171,7 @@ signature = md5(agent_id + secret + time)
|
|||||||
|
|
||||||
- **路径**:`/api/v1/getPlayerGameRecord`
|
- **路径**:`/api/v1/getPlayerGameRecord`
|
||||||
- **方法**:POST
|
- **方法**:POST
|
||||||
- **请求头**:`auth-token`
|
- **请求头**:`api-key`、`auth-token`
|
||||||
|
|
||||||
#### 请求参数
|
#### 请求参数
|
||||||
|
|
||||||
@@ -181,7 +192,7 @@ signature = md5(agent_id + secret + time)
|
|||||||
|
|
||||||
- **路径**:`/api/v1/getPlayerWalletRecord`
|
- **路径**:`/api/v1/getPlayerWalletRecord`
|
||||||
- **方法**:POST
|
- **方法**:POST
|
||||||
- **请求头**:`auth-token`
|
- **请求头**:`api-key`、`auth-token`
|
||||||
|
|
||||||
参数与时间规则同 3.3(无 `page`,仅 `limit` 限制条数),返回钱包流水列表(附带 `dice_player`)。
|
参数与时间规则同 3.3(无 `page`,仅 `limit` 限制条数),返回钱包流水列表(附带 `dice_player`)。
|
||||||
|
|
||||||
@@ -189,7 +200,7 @@ signature = md5(agent_id + secret + time)
|
|||||||
|
|
||||||
- **路径**:`/api/v1/getPlayerTicketRecord`
|
- **路径**:`/api/v1/getPlayerTicketRecord`
|
||||||
- **方法**:POST
|
- **方法**:POST
|
||||||
- **请求头**:`auth-token`
|
- **请求头**:`api-key`、`auth-token`
|
||||||
|
|
||||||
参数与时间规则同 3.3,返回中奖券记录列表(附带 `dice_player`)。
|
参数与时间规则同 3.3,返回中奖券记录列表(附带 `dice_player`)。
|
||||||
|
|
||||||
@@ -197,7 +208,7 @@ signature = md5(agent_id + secret + time)
|
|||||||
|
|
||||||
- **路径**:`/api/v1/setPlayerWallet`
|
- **路径**:`/api/v1/setPlayerWallet`
|
||||||
- **方法**:POST
|
- **方法**:POST
|
||||||
- **请求头**:`auth-token`
|
- **请求头**:`api-key`、`auth-token`
|
||||||
- **说明**:平台为玩家加币/扣币,生成钱包流水。
|
- **说明**:平台为玩家加币/扣币,生成钱包流水。
|
||||||
|
|
||||||
#### 请求参数
|
#### 请求参数
|
||||||
@@ -256,9 +267,9 @@ signature = md5(agent_id + secret + time)
|
|||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| 200 | 成功 | 请求成功 |
|
| 200 | 成功 | 请求成功 |
|
||||||
| 400 | 请求参数错误 | 缺参、参数格式不合法、范围错误 |
|
| 400 | 请求参数错误 | 缺参、参数格式不合法、范围错误 |
|
||||||
| 401 | 未授权 | 未携带 `auth-token` 或 `token` |
|
| 401 | 未授权 | 未携带 `api-key`、`auth-token` 或 `token` |
|
||||||
| 402 | token 无效或已过期 | `auth-token/token` 过期、签名错误、被挤下线等 |
|
| 402 | token 无效或已过期 | `auth-token/token` 过期、签名错误、被挤下线等 |
|
||||||
| 403 | 鉴权失败 | `secret` 错误、签名验证失败、时间戳无效等 |
|
| 403 | 鉴权失败 | `api-key` 无效、`secret` 错误、签名验证失败、时间戳无效等 |
|
||||||
| 404 | 资源不存在 | 用户不存在等 |
|
| 404 | 资源不存在 | 用户不存在等 |
|
||||||
| 422 | 业务逻辑错误 | 余额不足、业务校验失败等 |
|
| 422 | 业务逻辑错误 | 余额不足、业务校验失败等 |
|
||||||
| 500 | 服务器内部错误 | 服务端异常或配置缺失 |
|
| 500 | 服务器内部错误 | 服务端异常或配置缺失 |
|
||||||
|
|||||||
@@ -128,7 +128,7 @@
|
|||||||
|
|
||||||
| 文档 | 内容 |
|
| 文档 | 内容 |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| [`API对接文档.md`](API对接文档.md) | 平台 `/api/v1/*`(`auth-token`)、玩家 `/api/*`(`token`)、统一返回码、联调建议。 |
|
| [`API对接文档.md`](API对接文档.md) | 平台 `/api/v1/*`(`api-key` + `auth-token`)、玩家 `/api/*`(`token`)、统一返回码、联调建议。 |
|
||||||
| `server/docs/` | 性能、权重测试、出点分析等专项说明(按需阅读)。 |
|
| `server/docs/` | 性能、权重测试、出点分析等专项说明(按需阅读)。 |
|
||||||
|
|
||||||
**与玩法直接相关的玩家接口示例**:
|
**与玩法直接相关的玩家接口示例**:
|
||||||
|
|||||||
@@ -8,7 +8,8 @@
|
|||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite --open",
|
"dev": "vite --open",
|
||||||
"build": "vue-tsc --noEmit && vite build",
|
"sync:flowcharts": "node scripts/sync-dice-flowcharts.mjs",
|
||||||
|
"build": "node scripts/sync-dice-flowcharts.mjs && vue-tsc --noEmit && vite build",
|
||||||
"serve": "vite preview",
|
"serve": "vite preview",
|
||||||
"lint": "eslint",
|
"lint": "eslint",
|
||||||
"fix": "eslint --fix",
|
"fix": "eslint --fix",
|
||||||
|
|||||||
192
saiadmin-artd/public/docs/flowcharts/dice-为何抽到该奖励.html
Normal file
@@ -0,0 +1,192 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<title>为何最终抽到该奖励</title>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.min.js"></script>
|
||||||
|
<style>
|
||||||
|
* { box-sizing: border-box; }
|
||||||
|
body {
|
||||||
|
font-family: "Microsoft YaHei", "PingFang SC", sans-serif;
|
||||||
|
margin: 0;
|
||||||
|
padding: 24px 32px 48px;
|
||||||
|
background: #f5f7fa;
|
||||||
|
color: #1a1a2e;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
header { max-width: 1100px; margin: 0 auto 16px; }
|
||||||
|
h1 { font-size: 1.5rem; margin: 0 0 8px; font-weight: 600; }
|
||||||
|
.subtitle { color: #5c6370; font-size: 0.95rem; margin: 0; }
|
||||||
|
.card {
|
||||||
|
max-width: 1100px;
|
||||||
|
margin: 0 auto;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 28px 24px;
|
||||||
|
box-shadow: 0 2px 12px rgba(0,0,0,.06);
|
||||||
|
}
|
||||||
|
.copy-hint {
|
||||||
|
max-width: 1100px;
|
||||||
|
margin: 12px auto 0;
|
||||||
|
font-size: 0.88rem;
|
||||||
|
color: #606266;
|
||||||
|
}
|
||||||
|
.copy-box {
|
||||||
|
max-width: 1100px;
|
||||||
|
margin: 8px auto 0;
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #dcdfe6;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 12px 14px;
|
||||||
|
}
|
||||||
|
.copy-box summary { cursor: pointer; font-size: 0.9rem; color: #409eff; }
|
||||||
|
.copy-box pre {
|
||||||
|
margin: 10px 0 0;
|
||||||
|
font-size: 0.78rem;
|
||||||
|
line-height: 1.45;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-all;
|
||||||
|
max-height: 280px;
|
||||||
|
overflow: auto;
|
||||||
|
color: #303133;
|
||||||
|
}
|
||||||
|
.legend {
|
||||||
|
max-width: 1100px;
|
||||||
|
margin: 20px auto 0;
|
||||||
|
padding: 16px 20px;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: #444;
|
||||||
|
}
|
||||||
|
.legend h2 { font-size: 1rem; margin: 0 0 10px; }
|
||||||
|
.legend ul { margin: 0; padding-left: 1.2em; }
|
||||||
|
.legend li { margin: 4px 0; }
|
||||||
|
.mermaid { display: flex; justify-content: center; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header>
|
||||||
|
<h1>为何最终抽到的是这个奖励</h1>
|
||||||
|
<p class="subtitle">业务说明:一局抽奖从开局到到账的决策顺序(仅用本项目菜单与业务用语)</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<p class="copy-hint">复制方式:展开下方「Mermaid 源码」全选复制,粘贴到 ProcessOn / draw.io / 飞书文档等支持 Mermaid 的流程图工具;或直接用浏览器打开本页看图。</p>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<pre class="mermaid">
|
||||||
|
flowchart TD
|
||||||
|
Start([玩家开始一局抽奖]) --> Dir[选择方向:顺时针 或 逆时针]
|
||||||
|
Dir --> Ante[选择底注倍数]
|
||||||
|
Ante --> Type{本局是否使用免费抽奖券?}
|
||||||
|
|
||||||
|
Type -->|是| Free[免费局]
|
||||||
|
Type -->|否且平台币足够| Paid[付费局:扣除底注对应平台币]
|
||||||
|
|
||||||
|
Free --> PoolKill[按「杀分奖池」的 T1~T5 档位概率抽签]
|
||||||
|
Paid --> KillCheck{彩金池已开启杀分<br/>且彩金池累计盈利 ≥ 安全线?}
|
||||||
|
KillCheck -->|是| PoolKill
|
||||||
|
KillCheck -->|否| PlayerW[按该玩家在「玩家管理」<br/>配置的 T1~T5 档位概率抽签]
|
||||||
|
|
||||||
|
PoolKill --> DrawTier[随机抽出档位 T1~T5]
|
||||||
|
PlayerW --> DrawTier
|
||||||
|
|
||||||
|
DrawTier --> PickRow[在「色子奖励权重」中<br/>取该档位 + 本局方向的所有行<br/>按行权重随机一条]
|
||||||
|
PickRow --> Got[得到:色子点数、结算金额、所属档位、落点格位]
|
||||||
|
|
||||||
|
Got --> KillMode{本局是否走杀分档位概率?}
|
||||||
|
KillMode -->|是| NoLeo[不发放豹子大奖<br/>且不会抽到仅能豹子的点数 5、30]
|
||||||
|
KillMode -->|否| NormalPath[按普通规则继续]
|
||||||
|
NoLeo --> DiceShow[生成五颗骰子并结算]
|
||||||
|
|
||||||
|
NormalPath --> Leopard{色子点数是否为<br/>5 / 10 / 15 / 20 / 25 / 30?}
|
||||||
|
Leopard -->|否| NormalWin[五颗骰子点数和 = 该点数<br/>奖金 = 结算金额 × 底注]
|
||||||
|
Leopard -->|是| LeoRule{点数?}
|
||||||
|
LeoRule -->|5 或 30| MustBig[必定豹子大奖]
|
||||||
|
LeoRule -->|10 / 15 / 20 / 25| BigRate[按「奖励配置」页签「大奖权重」<br/>该点数权重决定真豹子或普通展示]
|
||||||
|
MustBig --> BigPay[豹子奖金 = 大奖结算金额 × 底注<br/>本局不再发该点数的普通奖]
|
||||||
|
BigRate -->|命中豹子| BigPay
|
||||||
|
BigRate -->|未中豹子| NonLeo[五颗骰子为非豹子组合<br/>奖金 = 结算金额 × 底注]
|
||||||
|
|
||||||
|
NormalWin --> T5Check
|
||||||
|
NonLeo --> T5Check
|
||||||
|
BigPay --> EndBig([本局结束:以豹子大奖为准])
|
||||||
|
DiceShow --> T5Check{档位为 T5 再来一次?}
|
||||||
|
T5Check -->|是| FreeTicket[赠送 1 次免费抽奖券<br/>下次免费局须相同底注]
|
||||||
|
T5Check -->|否| EndNormal([本局结束:以普通奖或惩罚为准])
|
||||||
|
FreeTicket --> EndNormal
|
||||||
|
|
||||||
|
style Start fill:#e8f4fc
|
||||||
|
style EndNormal fill:#e8fce8
|
||||||
|
style EndBig fill:#fff3e0
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<details class="copy-box">
|
||||||
|
<summary>Mermaid 源码(可复制,与同目录 .mmd 文件一致)</summary>
|
||||||
|
<pre id="mermaid-src">flowchart TD
|
||||||
|
Start([玩家开始一局抽奖]) --> Dir[选择方向:顺时针 或 逆时针]
|
||||||
|
Dir --> Ante[选择底注倍数]
|
||||||
|
Ante --> Type{本局是否使用免费抽奖券?}
|
||||||
|
|
||||||
|
Type -->|是| Free[免费局]
|
||||||
|
Type -->|否且平台币足够| Paid[付费局:扣除底注对应平台币]
|
||||||
|
|
||||||
|
Free --> PoolKill[按「杀分奖池」的 T1~T5 档位概率抽签]
|
||||||
|
Paid --> KillCheck{彩金池已开启杀分<br/>且彩金池累计盈利 ≥ 安全线?}
|
||||||
|
KillCheck -->|是| PoolKill
|
||||||
|
KillCheck -->|否| PlayerW[按该玩家在「玩家管理」<br/>配置的 T1~T5 档位概率抽签]
|
||||||
|
|
||||||
|
PoolKill --> DrawTier[随机抽出档位 T1~T5]
|
||||||
|
PlayerW --> DrawTier
|
||||||
|
|
||||||
|
DrawTier --> PickRow[在「色子奖励权重」中<br/>取该档位 + 本局方向的所有行<br/>按行权重随机一条]
|
||||||
|
PickRow --> Got[得到:色子点数、结算金额、所属档位、落点格位]
|
||||||
|
|
||||||
|
Got --> KillMode{本局是否走杀分档位概率?}
|
||||||
|
KillMode -->|是| NoLeo[不发放豹子大奖<br/>且不会抽到仅能豹子的点数 5、30]
|
||||||
|
KillMode -->|否| NormalPath[按普通规则继续]
|
||||||
|
NoLeo --> DiceShow[生成五颗骰子并结算]
|
||||||
|
|
||||||
|
NormalPath --> Leopard{色子点数是否为<br/>5 / 10 / 15 / 20 / 25 / 30?}
|
||||||
|
Leopard -->|否| NormalWin[五颗骰子点数和 = 该点数<br/>奖金 = 结算金额 × 底注]
|
||||||
|
Leopard -->|是| LeoRule{点数?}
|
||||||
|
LeoRule -->|5 或 30| MustBig[必定豹子大奖]
|
||||||
|
LeoRule -->|10 / 15 / 20 / 25| BigRate[按「奖励配置」页签「大奖权重」<br/>该点数权重决定真豹子或普通展示]
|
||||||
|
MustBig --> BigPay[豹子奖金 = 大奖结算金额 × 底注<br/>本局不再发该点数的普通奖]
|
||||||
|
BigRate -->|命中豹子| BigPay
|
||||||
|
BigRate -->|未中豹子| NonLeo[五颗骰子为非豹子组合<br/>奖金 = 结算金额 × 底注]
|
||||||
|
|
||||||
|
NormalWin --> T5Check
|
||||||
|
NonLeo --> T5Check
|
||||||
|
BigPay --> EndBig([本局结束:以豹子大奖为准])
|
||||||
|
DiceShow --> T5Check{档位为 T5 再来一次?}
|
||||||
|
T5Check -->|是| FreeTicket[赠送 1 次免费抽奖券<br/>下次免费局须相同底注]
|
||||||
|
T5Check -->|否| EndNormal([本局结束:以普通奖或惩罚为准])
|
||||||
|
FreeTicket --> EndNormal
|
||||||
|
|
||||||
|
style Start fill:#e8f4fc
|
||||||
|
style EndNormal fill:#e8fce8
|
||||||
|
style EndBig fill:#fff3e0</pre>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<div class="legend">
|
||||||
|
<h2>读图要点</h2>
|
||||||
|
<ul>
|
||||||
|
<li><strong>两步抽签</strong>:先抽档位 T1~T5(大奖 / 小赚 / 抽水 / 惩罚 / 再来一次),再在该档位 + 方向的多条奖励里按权重抽具体点数与结算金额。</li>
|
||||||
|
<li><strong>免费局与杀分局</strong>:都用杀分奖池的档位概率;一般不会出豹子大奖,也不会抽到只能组成豹子的点数 5、30。</li>
|
||||||
|
<li><strong>普通付费局</strong>:彩金池未到杀分条件时,用该玩家在「玩家管理」里的档位权重,才可能按「大奖权重」出豹子。</li>
|
||||||
|
<li><strong>玩家最终看到</strong>:色子点数、五颗骰子图案、到账平台币(普通奖 + 豹子奖)、是否获得「再来一次」免费券。</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
mermaid.initialize({
|
||||||
|
startOnLoad: true,
|
||||||
|
theme: 'neutral',
|
||||||
|
flowchart: { curve: 'basis', padding: 16, nodeSpacing: 28, rankSpacing: 40 }
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
44
saiadmin-artd/public/docs/flowcharts/dice-为何抽到该奖励.mmd
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
flowchart TD
|
||||||
|
Start([玩家开始一局抽奖]) --> Dir[选择方向:顺时针 或 逆时针]
|
||||||
|
Dir --> Ante[选择底注倍数]
|
||||||
|
Ante --> Type{本局是否使用免费抽奖券?}
|
||||||
|
|
||||||
|
Type -->|是| Free[免费局]
|
||||||
|
Type -->|否且平台币足够| Paid[付费局:扣除底注对应平台币]
|
||||||
|
|
||||||
|
Free --> PoolKill[按「杀分奖池」的 T1~T5 档位概率抽签]
|
||||||
|
Paid --> KillCheck{彩金池已开启杀分<br/>且彩金池累计盈利 ≥ 安全线?}
|
||||||
|
KillCheck -->|是| PoolKill
|
||||||
|
KillCheck -->|否| PlayerW[按该玩家在「玩家管理」<br/>配置的 T1~T5 档位概率抽签]
|
||||||
|
|
||||||
|
PoolKill --> DrawTier[随机抽出档位 T1~T5]
|
||||||
|
PlayerW --> DrawTier
|
||||||
|
|
||||||
|
DrawTier --> PickRow[在「色子奖励权重」中<br/>取该档位 + 本局方向的所有行<br/>按行权重随机一条]
|
||||||
|
PickRow --> Got[得到:色子点数、结算金额、所属档位、落点格位]
|
||||||
|
|
||||||
|
Got --> KillMode{本局是否走杀分档位概率?}
|
||||||
|
KillMode -->|是| NoLeo[不发放豹子大奖<br/>且不会抽到仅能豹子的点数 5、30]
|
||||||
|
KillMode -->|否| NormalPath[按普通规则继续]
|
||||||
|
NoLeo --> DiceShow[生成五颗骰子并结算]
|
||||||
|
|
||||||
|
NormalPath --> Leopard{色子点数是否为<br/>5 / 10 / 15 / 20 / 25 / 30?}
|
||||||
|
Leopard -->|否| NormalWin[五颗骰子点数和 = 该点数<br/>奖金 = 结算金额 × 底注]
|
||||||
|
Leopard -->|是| LeoRule{点数?}
|
||||||
|
LeoRule -->|5 或 30| MustBig[必定豹子大奖]
|
||||||
|
LeoRule -->|10 / 15 / 20 / 25| BigRate[按「奖励配置」页签「大奖权重」<br/>该点数权重决定真豹子或普通展示]
|
||||||
|
MustBig --> BigPay[豹子奖金 = 大奖结算金额 × 底注<br/>本局不再发该点数的普通奖]
|
||||||
|
BigRate -->|命中豹子| BigPay
|
||||||
|
BigRate -->|未中豹子| NonLeo[五颗骰子为非豹子组合<br/>奖金 = 结算金额 × 底注]
|
||||||
|
|
||||||
|
NormalWin --> T5Check
|
||||||
|
NonLeo --> T5Check
|
||||||
|
BigPay --> EndBig([本局结束:以豹子大奖为准])
|
||||||
|
DiceShow --> T5Check{档位为 T5 再来一次?}
|
||||||
|
T5Check -->|是| FreeTicket[赠送 1 次免费抽奖券<br/>下次免费局须相同底注]
|
||||||
|
T5Check -->|否| EndNormal([本局结束:以普通奖或惩罚为准])
|
||||||
|
FreeTicket --> EndNormal
|
||||||
|
|
||||||
|
style Start fill:#e8f4fc
|
||||||
|
style EndNormal fill:#e8fce8
|
||||||
|
style EndBig fill:#fff3e0
|
||||||
219
saiadmin-artd/public/docs/flowcharts/dice-后台中奖逻辑配置.html
Normal file
@@ -0,0 +1,219 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<title>后台如何配置中奖逻辑</title>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.min.js"></script>
|
||||||
|
<style>
|
||||||
|
* { box-sizing: border-box; }
|
||||||
|
body {
|
||||||
|
font-family: "Microsoft YaHei", "PingFang SC", sans-serif;
|
||||||
|
margin: 0;
|
||||||
|
padding: 24px 32px 48px;
|
||||||
|
background: #f0f4f8;
|
||||||
|
color: #1a1a2e;
|
||||||
|
line-height: 1.55;
|
||||||
|
}
|
||||||
|
header { max-width: 1200px; margin: 0 auto 16px; }
|
||||||
|
h1 { font-size: 1.45rem; margin: 0 0 6px; font-weight: 600; }
|
||||||
|
.subtitle { color: #5c6370; font-size: 0.92rem; margin: 0; }
|
||||||
|
.tip {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto 16px;
|
||||||
|
padding: 12px 16px;
|
||||||
|
background: #fff8e6;
|
||||||
|
border-left: 4px solid #e6a23c;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
.card {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto 20px;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 24px 20px;
|
||||||
|
box-shadow: 0 2px 12px rgba(0,0,0,.06);
|
||||||
|
}
|
||||||
|
.card h2 { font-size: 1.05rem; margin: 0 0 12px; color: #303133; }
|
||||||
|
.copy-hint {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto 12px;
|
||||||
|
font-size: 0.88rem;
|
||||||
|
color: #606266;
|
||||||
|
}
|
||||||
|
.copy-box {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 8px auto 20px;
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #dcdfe6;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 12px 14px;
|
||||||
|
}
|
||||||
|
.copy-box summary { cursor: pointer; font-size: 0.9rem; color: #409eff; }
|
||||||
|
.copy-box pre {
|
||||||
|
margin: 10px 0 0;
|
||||||
|
font-size: 0.76rem;
|
||||||
|
line-height: 1.4;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-all;
|
||||||
|
max-height: 320px;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
.steps {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
display: grid;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
.step {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 12px 16px;
|
||||||
|
border: 1px solid #e4e7ed;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
.step strong { color: #409eff; }
|
||||||
|
.mermaid { display: flex; justify-content: center; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header>
|
||||||
|
<h1>后台如何配置中奖逻辑</h1>
|
||||||
|
<p class="subtitle">按「一局真实抽奖」顺序:每个环节对应左侧菜单与页面按钮(与前台逻辑一致)</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<p class="tip">菜单根目录:<strong>大富翁-色子游戏</strong>。多渠道后台请先选顶部<strong>渠道</strong>,再改该渠道数据。</p>
|
||||||
|
<p class="copy-hint">复制:展开「Mermaid 源码」粘贴到流程图工具;日常维护也可打开同目录 <code>dice-后台中奖逻辑配置.mmd</code>。</p>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h2>主流程图(抽奖环节 → 去哪点哪个按钮)</h2>
|
||||||
|
<pre class="mermaid">
|
||||||
|
flowchart TD
|
||||||
|
O([按一局抽奖的真实顺序配置后台]) --> L1
|
||||||
|
|
||||||
|
L1[① 玩家选方向 + 底注] --> L1A[可选:大富翁-色子游戏 → 底注配置<br/>按钮:新增 / 行内编辑 → 提交]
|
||||||
|
L1A --> L2
|
||||||
|
|
||||||
|
L2[② 先随机抽出档位 T1~T5] --> L2Q{本局类型?}
|
||||||
|
L2Q -->|免费抽奖券| L2F[概率来源:杀分奖池 killScore]
|
||||||
|
L2Q -->|付费且彩金池杀分生效| L2F
|
||||||
|
L2Q -->|付费且未杀分| L2P[概率来源:该玩家档位权重]
|
||||||
|
|
||||||
|
L2F --> M2F[大富翁-色子游戏 → 彩金池配置<br/>按钮:行内「编辑」→ 名称 killScore<br/>填写 T1池权重~T5池权重 合计 100%<br/>按钮:「提交」]
|
||||||
|
L2P --> M2P[大富翁-色子游戏 → 玩家管理<br/>按钮:行内「编辑」<br/>填写 T1池权重~T5池权重 或 选择「彩金池配置」<br/>按钮:「提交」]
|
||||||
|
|
||||||
|
M2F --> L2K
|
||||||
|
M2P --> L2K
|
||||||
|
L2K[杀分何时对付费局生效] --> M2K[大富翁-色子游戏 → 彩金池配置<br/>按钮:「查看当前彩金池」<br/>填写「安全线」· 开关「开启杀分」<br/>按钮:「保存安全线」]
|
||||||
|
|
||||||
|
M2K --> L3
|
||||||
|
L3[③ 在档位内随机一条奖励行] --> M3A[须先有盘面金额与档位规则]
|
||||||
|
M3A --> M3B[大富翁-色子游戏 → 奖励配置<br/>页签「奖励索引」→ 填写结算金额等<br/>按钮:「保存」]
|
||||||
|
M3B --> M3C[奖励配置 → 按钮「创建奖励对照」<br/>弹窗 → 按钮「确认导入」]
|
||||||
|
M3C --> M3D[大富翁-色子游戏 → 色子奖励权重<br/>按钮:「权重配比」→ 页签顺时针/逆时针<br/>按 T1~T5 填各点数权重 → 按钮「提交」]
|
||||||
|
|
||||||
|
M3D --> L4
|
||||||
|
L4[④ 若抽到豹子点数 5/10/15/20/25/30] --> L4Q{本局是否杀分档位?}
|
||||||
|
L4Q -->|是| L4N[不触发豹子大奖]
|
||||||
|
L4Q -->|否| L4Y[可能触发豹子大奖]
|
||||||
|
L4Y --> M4[大富翁-色子游戏 → 奖励配置<br/>页签「大奖权重」→ 拖动权重滑条<br/>按钮:「保存」<br/>说明:点数 5、30 固定必中;10/15/20/25 可调]
|
||||||
|
|
||||||
|
L4N --> L5
|
||||||
|
M4 --> L5
|
||||||
|
L5[⑤ 验证后上线] --> M5A[色子奖励权重 → 按钮「一键测试权重」<br/>弹窗 → 按钮「开始测试」]
|
||||||
|
M5A --> M5B[权重测试记录 → 按钮「查看详情」<br/>按钮「导入到当前配置」→「确认导入」]
|
||||||
|
M5B --> Done([可对玩家开放;用「玩家抽奖记录」核对])
|
||||||
|
|
||||||
|
style O fill:#e8f4fc
|
||||||
|
style Done fill:#e8fce8
|
||||||
|
style M2F fill:#fdf6ec
|
||||||
|
style M2P fill:#fdf6ec
|
||||||
|
style M3D fill:#fde2e2
|
||||||
|
style M4 fill:#e1f3d8
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<details class="copy-box">
|
||||||
|
<summary>Mermaid 源码(可复制,与同目录 .mmd 文件一致)</summary>
|
||||||
|
<pre id="mermaid-src">flowchart TD
|
||||||
|
O([按一局抽奖的真实顺序配置后台]) --> L1
|
||||||
|
|
||||||
|
L1[① 玩家选方向 + 底注] --> L1A[可选:大富翁-色子游戏 → 底注配置<br/>按钮:新增 / 行内编辑 → 提交]
|
||||||
|
L1A --> L2
|
||||||
|
|
||||||
|
L2[② 先随机抽出档位 T1~T5] --> L2Q{本局类型?}
|
||||||
|
L2Q -->|免费抽奖券| L2F[概率来源:杀分奖池 killScore]
|
||||||
|
L2Q -->|付费且彩金池杀分生效| L2F
|
||||||
|
L2Q -->|付费且未杀分| L2P[概率来源:该玩家档位权重]
|
||||||
|
|
||||||
|
L2F --> M2F[大富翁-色子游戏 → 彩金池配置<br/>按钮:行内「编辑」→ 名称 killScore<br/>填写 T1池权重~T5池权重 合计 100%<br/>按钮:「提交」]
|
||||||
|
L2P --> M2P[大富翁-色子游戏 → 玩家管理<br/>按钮:行内「编辑」<br/>填写 T1池权重~T5池权重 或 选择「彩金池配置」<br/>按钮:「提交」]
|
||||||
|
|
||||||
|
M2F --> L2K
|
||||||
|
M2P --> L2K
|
||||||
|
L2K[杀分何时对付费局生效] --> M2K[大富翁-色子游戏 → 彩金池配置<br/>按钮:「查看当前彩金池」<br/>填写「安全线」· 开关「开启杀分」<br/>按钮:「保存安全线」]
|
||||||
|
|
||||||
|
M2K --> L3
|
||||||
|
L3[③ 在档位内随机一条奖励行] --> M3A[须先有盘面金额与档位规则]
|
||||||
|
M3A --> M3B[大富翁-色子游戏 → 奖励配置<br/>页签「奖励索引」→ 填写结算金额等<br/>按钮:「保存」]
|
||||||
|
M3B --> M3C[奖励配置 → 按钮「创建奖励对照」<br/>弹窗 → 按钮「确认导入」]
|
||||||
|
M3C --> M3D[大富翁-色子游戏 → 色子奖励权重<br/>按钮:「权重配比」→ 页签顺时针/逆时针<br/>按 T1~T5 填各点数权重 → 按钮「提交」]
|
||||||
|
|
||||||
|
M3D --> L4
|
||||||
|
L4[④ 若抽到豹子点数 5/10/15/20/25/30] --> L4Q{本局是否杀分档位?}
|
||||||
|
L4Q -->|是| L4N[不触发豹子大奖]
|
||||||
|
L4Q -->|否| L4Y[可能触发豹子大奖]
|
||||||
|
L4Y --> M4[大富翁-色子游戏 → 奖励配置<br/>页签「大奖权重」→ 拖动权重滑条<br/>按钮:「保存」<br/>说明:点数 5、30 固定必中;10/15/20/25 可调]
|
||||||
|
|
||||||
|
L4N --> L5
|
||||||
|
M4 --> L5
|
||||||
|
L5[⑤ 验证后上线] --> M5A[色子奖励权重 → 按钮「一键测试权重」<br/>弹窗 → 按钮「开始测试」]
|
||||||
|
M5A --> M5B[权重测试记录 → 按钮「查看详情」<br/>按钮「导入到当前配置」→「确认导入」]
|
||||||
|
M5B --> Done([可对玩家开放;用「玩家抽奖记录」核对])
|
||||||
|
|
||||||
|
style O fill:#e8f4fc
|
||||||
|
style Done fill:#e8fce8
|
||||||
|
style M2F fill:#fdf6ec
|
||||||
|
style M2P fill:#fdf6ec
|
||||||
|
style M3D fill:#fde2e2
|
||||||
|
style M4 fill:#e1f3d8</pre>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h2>首次搭建推荐顺序(与上图环节对应)</h2>
|
||||||
|
<pre class="mermaid">
|
||||||
|
flowchart TD
|
||||||
|
O([开始配置]) --> R1[奖励配置 · 页签「奖励索引」· 按钮「保存」]
|
||||||
|
R1 --> R2[奖励配置 · 页签「大奖权重」· 按钮「保存」]
|
||||||
|
R2 --> R3[奖励配置 · 按钮「创建奖励对照」·「确认导入」]
|
||||||
|
R3 --> W[色子奖励权重 · 按钮「权重配比」· 按钮「提交」]
|
||||||
|
W --> P1[彩金池配置 · 行内「编辑」default / killScore ·「提交」]
|
||||||
|
P1 --> P2[彩金池配置 ·「查看当前彩金池」·「保存安全线」]
|
||||||
|
P2 --> PL[玩家管理 · 行内「编辑」· 档位权重 ·「提交」]
|
||||||
|
PL --> T{要仿真?}
|
||||||
|
T -->|是| Test[色子奖励权重 ·「一键测试权重」·「开始测试」]
|
||||||
|
Test --> Imp[权重测试记录 ·「查看详情」·「导入到当前配置」·「确认导入」]
|
||||||
|
T -->|否| Live([上线])
|
||||||
|
Imp --> Live
|
||||||
|
|
||||||
|
style O fill:#e8f4fc
|
||||||
|
style Live fill:#e8fce8
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="steps">
|
||||||
|
<div class="step"><strong>档位含义</strong>:T1 大奖 · T2 小赚 · T3 抽水 · T4 惩罚 · T5 再来一次(由「奖励索引」结算金额规则决定,见页内说明)。</div>
|
||||||
|
<div class="step"><strong>改「奖励索引」后</strong>:必须再点「创建奖励对照」→「确认导入」,否则抽奖仍用旧对照表。</div>
|
||||||
|
<div class="step"><strong>核对真实对局</strong>:大富翁-色子游戏 → 玩家抽奖记录(看奖励档位、色子点数、摇色子中奖平台币、中大奖平台币、底注、方向)。</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
mermaid.initialize({
|
||||||
|
startOnLoad: true,
|
||||||
|
theme: 'neutral',
|
||||||
|
flowchart: { curve: 'basis', padding: 14, nodeSpacing: 24, rankSpacing: 36 }
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
42
saiadmin-artd/public/docs/flowcharts/dice-后台中奖逻辑配置.mmd
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
flowchart TD
|
||||||
|
O([按一局抽奖的真实顺序配置后台]) --> L1
|
||||||
|
|
||||||
|
L1[① 玩家选方向 + 底注] --> L1A[可选:大富翁-色子游戏 → 底注配置<br/>按钮:新增 / 行内编辑 → 提交]
|
||||||
|
L1A --> L2
|
||||||
|
|
||||||
|
L2[② 先随机抽出档位 T1~T5] --> L2Q{本局类型?}
|
||||||
|
L2Q -->|免费抽奖券| L2F[概率来源:杀分奖池 killScore]
|
||||||
|
L2Q -->|付费且彩金池杀分生效| L2F
|
||||||
|
L2Q -->|付费且未杀分| L2P[概率来源:该玩家档位权重]
|
||||||
|
|
||||||
|
L2F --> M2F[大富翁-色子游戏 → 彩金池配置<br/>按钮:行内「编辑」→ 名称 killScore<br/>填写 T1池权重~T5池权重 合计 100%<br/>按钮:「提交」]
|
||||||
|
L2P --> M2P[大富翁-色子游戏 → 玩家管理<br/>按钮:行内「编辑」<br/>填写 T1池权重~T5池权重 或 选择「彩金池配置」<br/>按钮:「提交」]
|
||||||
|
|
||||||
|
M2F --> L2K
|
||||||
|
M2P --> L2K
|
||||||
|
L2K[杀分何时对付费局生效] --> M2K[大富翁-色子游戏 → 彩金池配置<br/>按钮:「查看当前彩金池」<br/>填写「安全线」· 开关「开启杀分」<br/>按钮:「保存安全线」]
|
||||||
|
|
||||||
|
M2K --> L3
|
||||||
|
L3[③ 在档位内随机一条奖励行] --> M3A[须先有盘面金额与档位规则]
|
||||||
|
M3A --> M3B[大富翁-色子游戏 → 奖励配置<br/>页签「奖励索引」→ 填写结算金额等<br/>按钮:「保存」]
|
||||||
|
M3B --> M3C[奖励配置 → 按钮「创建奖励对照」<br/>弹窗 → 按钮「确认导入」]
|
||||||
|
M3C --> M3D[大富翁-色子游戏 → 色子奖励权重<br/>按钮:「权重配比」→ 页签顺时针/逆时针<br/>按 T1~T5 填各点数权重 → 按钮「提交」]
|
||||||
|
|
||||||
|
M3D --> L4
|
||||||
|
L4[④ 若抽到豹子点数 5/10/15/20/25/30] --> L4Q{本局是否杀分档位?}
|
||||||
|
L4Q -->|是| L4N[不触发豹子大奖]
|
||||||
|
L4Q -->|否| L4Y[可能触发豹子大奖]
|
||||||
|
L4Y --> M4[大富翁-色子游戏 → 奖励配置<br/>页签「大奖权重」→ 拖动权重滑条<br/>按钮:「保存」<br/>说明:点数 5、30 固定必中;10/15/20/25 可调]
|
||||||
|
|
||||||
|
L4N --> L5
|
||||||
|
M4 --> L5
|
||||||
|
L5[⑤ 验证后上线] --> M5A[色子奖励权重 → 按钮「一键测试权重」<br/>弹窗 → 按钮「开始测试」]
|
||||||
|
M5A --> M5B[权重测试记录 → 按钮「查看详情」<br/>按钮「导入到当前配置」→「确认导入」]
|
||||||
|
M5B --> Done([可对玩家开放;用「玩家抽奖记录」核对])
|
||||||
|
|
||||||
|
style O fill:#e8f4fc
|
||||||
|
style Done fill:#e8fce8
|
||||||
|
style M2F fill:#fdf6ec
|
||||||
|
style M2P fill:#fdf6ec
|
||||||
|
style M3D fill:#fde2e2
|
||||||
|
style M4 fill:#e1f3d8
|
||||||
26
saiadmin-artd/scripts/sync-dice-flowcharts.mjs
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
/**
|
||||||
|
* 将 server/docs/flowcharts 同步到 saiadmin-artd/public/docs/flowcharts
|
||||||
|
* 构建前执行,保证部署包内含最新流程图 HTML/MMD
|
||||||
|
*/
|
||||||
|
import fs from 'fs'
|
||||||
|
import path from 'path'
|
||||||
|
import { fileURLToPath } from 'url'
|
||||||
|
|
||||||
|
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
||||||
|
const artdRoot = path.resolve(__dirname, '..')
|
||||||
|
const repoRoot = path.resolve(artdRoot, '..')
|
||||||
|
const srcDir = path.join(repoRoot, 'server', 'docs', 'flowcharts')
|
||||||
|
const destDir = path.join(artdRoot, 'public', 'docs', 'flowcharts')
|
||||||
|
|
||||||
|
if (!fs.existsSync(srcDir)) {
|
||||||
|
console.warn('[sync-dice-flowcharts] 源目录不存在,跳过:', srcDir)
|
||||||
|
process.exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.mkdirSync(destDir, { recursive: true })
|
||||||
|
const names = fs.readdirSync(srcDir).filter((n) => /\.(html|mmd)$/i.test(n))
|
||||||
|
for (const name of names) {
|
||||||
|
fs.copyFileSync(path.join(srcDir, name), path.join(destDir, name))
|
||||||
|
console.log('[sync-dice-flowcharts] copied', name)
|
||||||
|
}
|
||||||
|
console.log('[sync-dice-flowcharts] done,', names.length, 'file(s)')
|
||||||
@@ -2,6 +2,8 @@ import request from '@/utils/http'
|
|||||||
|
|
||||||
export type DashboardQueryParams = {
|
export type DashboardQueryParams = {
|
||||||
dept_id?: number
|
dept_id?: number
|
||||||
|
/** 统计日期,格式 YYYY-MM-DD,默认当日 */
|
||||||
|
date?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
26
saiadmin-artd/src/api/system/admin_guide.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import request from '@/utils/http'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 后台操作指南 API
|
||||||
|
*/
|
||||||
|
export default {
|
||||||
|
/**
|
||||||
|
* 读取 Markdown 内容
|
||||||
|
*/
|
||||||
|
read() {
|
||||||
|
return request.get<Api.Common.ApiData>({
|
||||||
|
url: '/core/adminGuide/read'
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存 Markdown 内容
|
||||||
|
*/
|
||||||
|
save(params: { content: string }) {
|
||||||
|
return request.post<Api.Common.ApiData>({
|
||||||
|
url: '/core/adminGuide/save',
|
||||||
|
data: params,
|
||||||
|
showSuccessMessage: true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -59,7 +59,8 @@ export default {
|
|||||||
delete(params: Record<string, any>) {
|
delete(params: Record<string, any>) {
|
||||||
return request.del<any>({
|
return request.del<any>({
|
||||||
url: '/core/dept/destroy',
|
url: '/core/dept/destroy',
|
||||||
data: params
|
data: params,
|
||||||
|
showErrorMessage: false
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -25,8 +25,8 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-col flex-grow min-w-0 min-h-0">
|
<div class="flex flex-col flex-grow min-w-0 min-h-0">
|
||||||
<div v-if="selectedDeptLabel" class="channel-banner mb-3 text-sm text-g-500">
|
<div v-if="selectedDisplayLabel" class="channel-banner mb-3 text-sm text-g-500">
|
||||||
{{ bannerLabel }}:<b>{{ selectedDeptLabel }}</b>
|
{{ bannerLabel }}:<b>{{ selectedDisplayLabel }}</b>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col flex-1 min-h-0 min-w-0">
|
<div class="flex flex-col flex-1 min-h-0 min-w-0">
|
||||||
<slot />
|
<slot />
|
||||||
@@ -66,10 +66,10 @@
|
|||||||
treeData,
|
treeData,
|
||||||
selectedDeptId,
|
selectedDeptId,
|
||||||
loadingChannels,
|
loadingChannels,
|
||||||
selectedDeptLabel,
|
|
||||||
handleChannelClick,
|
handleChannelClick,
|
||||||
provideScope,
|
provideScope,
|
||||||
isConfigScope,
|
isConfigScope,
|
||||||
|
isAllChannelScope,
|
||||||
showDefaultTemplate
|
showDefaultTemplate
|
||||||
} = useChannelDeptScope()
|
} = useChannelDeptScope()
|
||||||
|
|
||||||
@@ -82,11 +82,17 @@
|
|||||||
const defaultLabel = isRoleChannelRoute(route)
|
const defaultLabel = isRoleChannelRoute(route)
|
||||||
? t('common.channelScope.defaultRoleTemplate')
|
? t('common.channelScope.defaultRoleTemplate')
|
||||||
: t('common.channelScope.defaultTemplate')
|
: t('common.channelScope.defaultTemplate')
|
||||||
|
const emptyNodeLabel = isAllChannelScope.value ? t('common.channelScope.allChannels') : defaultLabel
|
||||||
return nodes.map((node) =>
|
return nodes.map((node) =>
|
||||||
node.id === DEFAULT_CHANNEL_ID && !node.label ? { ...node, label: defaultLabel } : node
|
node.id === DEFAULT_CHANNEL_ID && !node.label ? { ...node, label: emptyNodeLabel } : node
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const selectedDisplayLabel = computed(() => {
|
||||||
|
const item = displayTreeData.value.find((node) => node.id === selectedDeptId.value)
|
||||||
|
return item?.label ?? ''
|
||||||
|
})
|
||||||
|
|
||||||
const bannerLabel = computed(() => {
|
const bannerLabel = computed(() => {
|
||||||
if (isConfigScope.value) {
|
if (isConfigScope.value) {
|
||||||
return t('common.channelScope.currentConfig')
|
return t('common.channelScope.currentConfig')
|
||||||
|
|||||||
@@ -1,8 +1,15 @@
|
|||||||
import type { InjectionKey, Ref, ComputedRef } from 'vue'
|
import type { InjectionKey, Ref, ComputedRef } from 'vue'
|
||||||
import { useRoute } from 'vue-router'
|
import { useRoute } from 'vue-router'
|
||||||
import deptApi from '@/api/system/dept'
|
import deptApi from '@/api/system/dept'
|
||||||
|
import { router } from '@/router'
|
||||||
import { useUserStore } from '@/store/modules/user'
|
import { useUserStore } from '@/store/modules/user'
|
||||||
import { isConfigChannelRoute, isRoleChannelRoute, isSuperAdminUser } from '@/utils/channelLayout'
|
import {
|
||||||
|
isAllChannelScopeRoute,
|
||||||
|
isConfigChannelRoute,
|
||||||
|
isNoChannelLayoutRoute,
|
||||||
|
isRoleChannelRoute,
|
||||||
|
isSuperAdminUser
|
||||||
|
} from '@/utils/channelLayout'
|
||||||
|
|
||||||
export interface ChannelTreeNode {
|
export interface ChannelTreeNode {
|
||||||
id: number
|
id: number
|
||||||
@@ -21,18 +28,42 @@ export interface ChannelDeptScopeContext {
|
|||||||
selectedDeptLabel: Ref<string>
|
selectedDeptLabel: Ref<string>
|
||||||
deptQueryParams: ComputedRef<{ dept_id: number }>
|
deptQueryParams: ComputedRef<{ dept_id: number }>
|
||||||
isConfigScope: ComputedRef<boolean>
|
isConfigScope: ComputedRef<boolean>
|
||||||
|
isAllChannelScope: ComputedRef<boolean>
|
||||||
showDefaultTemplate: ComputedRef<boolean>
|
showDefaultTemplate: ComputedRef<boolean>
|
||||||
}
|
}
|
||||||
|
|
||||||
export const CHANNEL_DEPT_SCOPE_KEY: InjectionKey<ChannelDeptScopeContext> =
|
export const CHANNEL_DEPT_SCOPE_KEY: InjectionKey<ChannelDeptScopeContext> =
|
||||||
Symbol('channelDeptScope')
|
Symbol('channelDeptScope')
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 当前应用中处于激活态的超管渠道上下文。
|
||||||
|
* 仅有一个 SuperAdminChannelShell 实例,所以可以在模块作用域内缓存其 ctx,
|
||||||
|
* 供 setInterval / 异步回调 / 事件处理器等"setup 外"路径取用——
|
||||||
|
* 这些路径上 Vue 的 inject() 会失效(getCurrentInstance() 为 null)。
|
||||||
|
*/
|
||||||
|
let activeChannelCtx: ChannelDeptScopeContext | null = null
|
||||||
|
|
||||||
export function provideChannelDeptScope(ctx: ChannelDeptScopeContext) {
|
export function provideChannelDeptScope(ctx: ChannelDeptScopeContext) {
|
||||||
provide(CHANNEL_DEPT_SCOPE_KEY, ctx)
|
provide(CHANNEL_DEPT_SCOPE_KEY, ctx)
|
||||||
|
activeChannelCtx = ctx
|
||||||
|
onScopeDispose(() => {
|
||||||
|
if (activeChannelCtx === ctx) {
|
||||||
|
activeChannelCtx = null
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useInjectedChannelDept(): ChannelDeptScopeContext | null {
|
export function useInjectedChannelDept(): ChannelDeptScopeContext | null {
|
||||||
return inject(CHANNEL_DEPT_SCOPE_KEY, null)
|
// 仅在组件 setup 同步路径下调用 inject() 才可靠;
|
||||||
|
// 否则(异步回调、setInterval、事件处理器等)退化为读取模块级激活上下文,
|
||||||
|
// 避免渠道切换后 getCurrentPool/withChannelDeptParams 等仍按超管自身部门发请求。
|
||||||
|
if (getCurrentInstance()) {
|
||||||
|
const ctx = inject(CHANNEL_DEPT_SCOPE_KEY, null)
|
||||||
|
if (ctx) {
|
||||||
|
return ctx
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return activeChannelCtx
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 超管全局渠道栏:创建并 provide 渠道上下文 */
|
/** 超管全局渠道栏:创建并 provide 渠道上下文 */
|
||||||
@@ -45,6 +76,7 @@ export function useChannelDeptScope() {
|
|||||||
|
|
||||||
const isConfigScope = computed(() => isConfigChannelRoute(route))
|
const isConfigScope = computed(() => isConfigChannelRoute(route))
|
||||||
const isRoleScope = computed(() => isRoleChannelRoute(route))
|
const isRoleScope = computed(() => isRoleChannelRoute(route))
|
||||||
|
const isAllChannelScope = computed(() => isAllChannelScopeRoute(route))
|
||||||
const showDefaultTemplate = computed(() => isConfigScope.value || isRoleScope.value)
|
const showDefaultTemplate = computed(() => isConfigScope.value || isRoleScope.value)
|
||||||
|
|
||||||
const isSuperAdmin = computed(() => isSuperAdminUser())
|
const isSuperAdmin = computed(() => isSuperAdminUser())
|
||||||
@@ -67,6 +99,9 @@ export function useChannelDeptScope() {
|
|||||||
|
|
||||||
const deptQueryParams = computed(() => {
|
const deptQueryParams = computed(() => {
|
||||||
const id = selectedDeptId.value
|
const id = selectedDeptId.value
|
||||||
|
if (isAllChannelScope.value && id <= 0) {
|
||||||
|
return { dept_id: 0 }
|
||||||
|
}
|
||||||
if (!showDefaultTemplate.value && id <= 0) {
|
if (!showDefaultTemplate.value && id <= 0) {
|
||||||
return { dept_id: 0 }
|
return { dept_id: 0 }
|
||||||
}
|
}
|
||||||
@@ -83,7 +118,7 @@ export function useChannelDeptScope() {
|
|||||||
label: String(item.label ?? item.name ?? item.id)
|
label: String(item.label ?? item.name ?? item.id)
|
||||||
}))
|
}))
|
||||||
if (isSuperAdmin.value) {
|
if (isSuperAdmin.value) {
|
||||||
if (showDefaultTemplate.value) {
|
if (showDefaultTemplate.value || isAllChannelScope.value) {
|
||||||
treeData.value = [{ id: DEFAULT_CHANNEL_ID, label: '' }, ...nodes]
|
treeData.value = [{ id: DEFAULT_CHANNEL_ID, label: '' }, ...nodes]
|
||||||
if (!treeData.value.some((n) => n.id === selectedDeptId.value)) {
|
if (!treeData.value.some((n) => n.id === selectedDeptId.value)) {
|
||||||
selectedDeptId.value = DEFAULT_CHANNEL_ID
|
selectedDeptId.value = DEFAULT_CHANNEL_ID
|
||||||
@@ -120,6 +155,7 @@ export function useChannelDeptScope() {
|
|||||||
selectedDeptLabel,
|
selectedDeptLabel,
|
||||||
deptQueryParams,
|
deptQueryParams,
|
||||||
isConfigScope,
|
isConfigScope,
|
||||||
|
isAllChannelScope,
|
||||||
showDefaultTemplate
|
showDefaultTemplate
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -147,6 +183,11 @@ export function bindChannelDeptToSearchParams(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const apply = (deptId: number) => {
|
const apply = (deptId: number) => {
|
||||||
|
if (channel.isAllChannelScope.value && deptId <= 0) {
|
||||||
|
delete searchParams.dept_id
|
||||||
|
refresh()
|
||||||
|
return
|
||||||
|
}
|
||||||
if (!channel.showDefaultTemplate.value && deptId <= 0) {
|
if (!channel.showDefaultTemplate.value && deptId <= 0) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -174,6 +215,10 @@ export function useChannelDeptReload(loadFn: () => void | Promise<void>) {
|
|||||||
watch(
|
watch(
|
||||||
() => channel.selectedDeptId.value,
|
() => channel.selectedDeptId.value,
|
||||||
(deptId) => {
|
(deptId) => {
|
||||||
|
if (channel.isAllChannelScope.value && deptId <= 0) {
|
||||||
|
void loadFn()
|
||||||
|
return
|
||||||
|
}
|
||||||
if (!channel.showDefaultTemplate.value && deptId <= 0) {
|
if (!channel.showDefaultTemplate.value && deptId <= 0) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -186,8 +231,15 @@ export function useChannelDeptReload(loadFn: () => void | Promise<void>) {
|
|||||||
/** 请求参数:业务页附带 dept_id;渠道管理员固定本渠道 */
|
/** 请求参数:业务页附带 dept_id;渠道管理员固定本渠道 */
|
||||||
export function getChannelDeptRequestParams(): { dept_id?: number } {
|
export function getChannelDeptRequestParams(): { dept_id?: number } {
|
||||||
const channel = useInjectedChannelDept()
|
const channel = useInjectedChannelDept()
|
||||||
|
const route = getCurrentInstance() ? useRoute() : router.currentRoute.value
|
||||||
|
if (isNoChannelLayoutRoute(route)) {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
if (channel?.isSuperAdmin.value) {
|
if (channel?.isSuperAdmin.value) {
|
||||||
const deptId = channel.selectedDeptId.value
|
const deptId = channel.selectedDeptId.value
|
||||||
|
if (channel.isAllChannelScope.value && deptId <= 0) {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
if (!channel.showDefaultTemplate.value && deptId <= 0) {
|
if (!channel.showDefaultTemplate.value && deptId <= 0) {
|
||||||
return {}
|
return {}
|
||||||
}
|
}
|
||||||
@@ -209,18 +261,25 @@ export function getChannelDeptRequestParams(): { dept_id?: number } {
|
|||||||
return {}
|
return {}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 保存/更新时附带 dept_id(优先渠道栏选中值,其次表单/行数据中的 dept_id) */
|
/** 保存/更新时附带 dept_id(新增优先渠道栏;更新优先行内 dept_id,避免默认模板 0 覆盖真实渠道) */
|
||||||
export function withChannelDeptParams<T extends Record<string, unknown>>(payload: T): T {
|
export function withChannelDeptParams<T extends Record<string, unknown>>(payload: T): T {
|
||||||
|
const rowDeptRaw = payload.dept_id
|
||||||
|
const hasRowDept =
|
||||||
|
rowDeptRaw !== undefined && rowDeptRaw !== null && rowDeptRaw !== ''
|
||||||
|
const rowDeptNum = hasRowDept ? Number(rowDeptRaw) : NaN
|
||||||
|
const isUpdate =
|
||||||
|
payload.id !== undefined && payload.id !== null && payload.id !== ''
|
||||||
|
|
||||||
|
if (isUpdate && hasRowDept && Number.isFinite(rowDeptNum) && rowDeptNum >= 0) {
|
||||||
|
return { ...payload, dept_id: rowDeptNum }
|
||||||
|
}
|
||||||
|
|
||||||
const extra = getChannelDeptRequestParams()
|
const extra = getChannelDeptRequestParams()
|
||||||
if ('dept_id' in extra) {
|
if ('dept_id' in extra) {
|
||||||
return { ...payload, ...extra }
|
return { ...payload, ...extra }
|
||||||
}
|
}
|
||||||
const rowDeptId = payload.dept_id
|
if (hasRowDept && Number.isFinite(rowDeptNum) && rowDeptNum > 0) {
|
||||||
if (rowDeptId !== undefined && rowDeptId !== null && rowDeptId !== '') {
|
return { ...payload, dept_id: rowDeptNum }
|
||||||
const num = Number(rowDeptId)
|
|
||||||
if (num > 0) {
|
|
||||||
return { ...payload, dept_id: num }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
const channel = useInjectedChannelDept()
|
const channel = useInjectedChannelDept()
|
||||||
if (channel && channel.selectedDeptId.value > 0) {
|
if (channel && channel.selectedDeptId.value > 0) {
|
||||||
|
|||||||
98
saiadmin-artd/src/composables/useDashboardScope.ts
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
import type { InjectionKey, Ref, ComputedRef } from 'vue'
|
||||||
|
import { getChannelDeptRequestParams, useInjectedChannelDept } from '@/composables/useChannelDeptScope'
|
||||||
|
|
||||||
|
export interface DashboardQueryParams {
|
||||||
|
dept_id?: number
|
||||||
|
date?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DashboardScopeContext {
|
||||||
|
selectedDate: Ref<string | null>
|
||||||
|
hasDateFilter: ComputedRef<boolean>
|
||||||
|
queryParams: ComputedRef<DashboardQueryParams>
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DASHBOARD_SCOPE_KEY: InjectionKey<DashboardScopeContext> = Symbol('dashboardScope')
|
||||||
|
|
||||||
|
function formatDateYmd(date: Date): string {
|
||||||
|
const y = date.getFullYear()
|
||||||
|
const m = String(date.getMonth() + 1).padStart(2, '0')
|
||||||
|
const d = String(date.getDate()).padStart(2, '0')
|
||||||
|
return `${y}-${m}-${d}`
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getTodayDateString(): string {
|
||||||
|
return formatDateYmd(new Date())
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 工作台页面 provide 日期筛选上下文 */
|
||||||
|
export function provideDashboardScope() {
|
||||||
|
const selectedDate = ref<string | null>(getTodayDateString())
|
||||||
|
const hasDateFilter = computed(() => !!selectedDate.value)
|
||||||
|
const queryParams = computed<DashboardQueryParams>(() => {
|
||||||
|
const params: DashboardQueryParams = {
|
||||||
|
...getChannelDeptRequestParams()
|
||||||
|
}
|
||||||
|
if (selectedDate.value) {
|
||||||
|
params.date = selectedDate.value
|
||||||
|
}
|
||||||
|
return params
|
||||||
|
})
|
||||||
|
const ctx: DashboardScopeContext = { selectedDate, hasDateFilter, queryParams }
|
||||||
|
provide(DASHBOARD_SCOPE_KEY, ctx)
|
||||||
|
return ctx
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useDashboardScope(): DashboardScopeContext {
|
||||||
|
const ctx = inject(DASHBOARD_SCOPE_KEY, null)
|
||||||
|
if (ctx) {
|
||||||
|
return ctx
|
||||||
|
}
|
||||||
|
const selectedDate = ref<string | null>(getTodayDateString())
|
||||||
|
const hasDateFilter = computed(() => !!selectedDate.value)
|
||||||
|
return {
|
||||||
|
selectedDate,
|
||||||
|
hasDateFilter,
|
||||||
|
queryParams: computed<DashboardQueryParams>(() => {
|
||||||
|
const params: DashboardQueryParams = {
|
||||||
|
...getChannelDeptRequestParams()
|
||||||
|
}
|
||||||
|
if (selectedDate.value) {
|
||||||
|
params.date = selectedDate.value
|
||||||
|
}
|
||||||
|
return params
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 工作台:渠道或日期变化时重新拉数 */
|
||||||
|
export function useDashboardReload(loadFn: () => void | Promise<void>) {
|
||||||
|
const channel = useInjectedChannelDept()
|
||||||
|
const { selectedDate } = useDashboardScope()
|
||||||
|
|
||||||
|
const run = () => {
|
||||||
|
void loadFn()
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(selectedDate, run)
|
||||||
|
|
||||||
|
if (!channel) {
|
||||||
|
onMounted(run)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => channel.selectedDeptId.value,
|
||||||
|
(deptId) => {
|
||||||
|
if (channel.isAllChannelScope.value && deptId <= 0) {
|
||||||
|
run()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!channel.showDefaultTemplate.value && deptId <= 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
run()
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -40,7 +40,11 @@ import { bindChannelDeptToSearchParams, useInjectedChannelDept, getChannelDeptRe
|
|||||||
// 类型推导工具类型
|
// 类型推导工具类型
|
||||||
type InferApiParams<T> = T extends (params: infer P) => any ? P : never
|
type InferApiParams<T> = T extends (params: infer P) => any ? P : never
|
||||||
type InferApiResponse<T> = T extends (params: any) => Promise<infer R> ? R : never
|
type InferApiResponse<T> = T extends (params: any) => Promise<infer R> ? R : never
|
||||||
type InferRecordType<T> = T extends Api.Common.PaginatedResponse<infer U> ? U : never
|
type InferRecordType<T> = T extends Api.Common.PaginatedResponse<infer U>
|
||||||
|
? U
|
||||||
|
: T extends Api.Common.ApiPage<infer U>
|
||||||
|
? U
|
||||||
|
: never
|
||||||
|
|
||||||
// 优化的配置接口 - 支持自动类型推导
|
// 优化的配置接口 - 支持自动类型推导
|
||||||
export interface UseTableConfig<
|
export interface UseTableConfig<
|
||||||
|
|||||||
@@ -42,6 +42,7 @@
|
|||||||
"listTitle": "Channels",
|
"listTitle": "Channels",
|
||||||
"defaultTemplate": "Default template",
|
"defaultTemplate": "Default template",
|
||||||
"defaultRoleTemplate": "Default role template",
|
"defaultRoleTemplate": "Default role template",
|
||||||
|
"allChannels": "All",
|
||||||
"currentConfig": "Current config",
|
"currentConfig": "Current config",
|
||||||
"currentChannel": "Current channel",
|
"currentChannel": "Current channel",
|
||||||
"currentRole": "Current roles"
|
"currentRole": "Current roles"
|
||||||
@@ -299,16 +300,33 @@
|
|||||||
"gohome": "Go Home"
|
"gohome": "Go Home"
|
||||||
},
|
},
|
||||||
"console": {
|
"console": {
|
||||||
|
"filter": {
|
||||||
|
"date": "Statistics Date",
|
||||||
|
"placeholder": "Select date",
|
||||||
|
"today": "Today",
|
||||||
|
"clear": "Clear"
|
||||||
|
},
|
||||||
"card": {
|
"card": {
|
||||||
"playerRegister": "Player Register",
|
"playerRegister": "Player Register",
|
||||||
"playerCharge": "Player Charge",
|
"playerCharge": "Player Charge",
|
||||||
"playerWithdraw": "Player Withdraw",
|
"playerWithdraw": "Player Withdraw",
|
||||||
"playerPlayCount": "Player Play Count",
|
"playerPlayCount": "Player Play Count",
|
||||||
"vsLastWeek": "vs Last Week"
|
"vsLastWeek": "vs Last Week",
|
||||||
|
"vsYesterday": "vs Yesterday",
|
||||||
|
"viewRechargeRecords": "View recharge records",
|
||||||
|
"viewWithdrawRecords": "View withdraw records",
|
||||||
|
"viewPlayRecords": "View play records",
|
||||||
|
"viewRegisterRecords": "View registered players"
|
||||||
|
},
|
||||||
|
"nav": {
|
||||||
|
"viewAllRecharge": "View all recharge",
|
||||||
|
"viewAllPlay": "View all play records",
|
||||||
|
"viewAllRegister": "View all registrations"
|
||||||
},
|
},
|
||||||
"newPlayer": {
|
"newPlayer": {
|
||||||
"title": "New Players",
|
"title": "New Players",
|
||||||
"subtitle": "Latest 50 new player records",
|
"subtitle": "Latest 50 new player records",
|
||||||
|
"subtitleByDate": "New players on {date} (up to 50)",
|
||||||
"player": "Player",
|
"player": "Player",
|
||||||
"balance": "Balance",
|
"balance": "Balance",
|
||||||
"ticket": "Tickets",
|
"ticket": "Tickets",
|
||||||
@@ -317,6 +335,7 @@
|
|||||||
"playRecord": {
|
"playRecord": {
|
||||||
"title": "Player Play Records",
|
"title": "Player Play Records",
|
||||||
"subtitle": "Latest 50 play records",
|
"subtitle": "Latest 50 play records",
|
||||||
|
"subtitleByDate": "Play records on {date} (up to 50)",
|
||||||
"player": "Player",
|
"player": "Player",
|
||||||
"reward": "Reward Tier",
|
"reward": "Reward Tier",
|
||||||
"winCoin": "Win Amount",
|
"winCoin": "Win Amount",
|
||||||
@@ -325,6 +344,7 @@
|
|||||||
"walletRecord": {
|
"walletRecord": {
|
||||||
"title": "Player Charge Records",
|
"title": "Player Charge Records",
|
||||||
"subtitle": "Latest 50 charge records",
|
"subtitle": "Latest 50 charge records",
|
||||||
|
"subtitleByDate": "Charge records on {date} (up to 50)",
|
||||||
"player": "Player",
|
"player": "Player",
|
||||||
"chargeAmount": "Amount",
|
"chargeAmount": "Amount",
|
||||||
"chargeTime": "Charge Time"
|
"chargeTime": "Charge Time"
|
||||||
@@ -427,6 +447,9 @@
|
|||||||
"rewardConfigRecord": "Dice Weight Test Records",
|
"rewardConfigRecord": "Dice Weight Test Records",
|
||||||
"playRecordTest": "Draw Records (Test Weight)",
|
"playRecordTest": "Draw Records (Test Weight)",
|
||||||
"config": "Game Config"
|
"config": "Game Config"
|
||||||
|
},
|
||||||
|
"game": {
|
||||||
|
"title": "Game Management"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"table": {
|
"table": {
|
||||||
@@ -446,6 +469,10 @@
|
|||||||
"max": "Max",
|
"max": "Max",
|
||||||
"startTime": "Start Time",
|
"startTime": "Start Time",
|
||||||
"endTime": "End Time",
|
"endTime": "End Time",
|
||||||
|
"quickDate": "Quick date",
|
||||||
|
"quickToday": "Today",
|
||||||
|
"quickYesterday": "Yesterday",
|
||||||
|
"quickLast7Days": "Last 7 days",
|
||||||
"placeholderUsername": "Username",
|
"placeholderUsername": "Username",
|
||||||
"placeholderNickname": "Nickname",
|
"placeholderNickname": "Nickname",
|
||||||
"placeholderPhone": "Phone",
|
"placeholderPhone": "Phone",
|
||||||
|
|||||||
@@ -27,6 +27,8 @@
|
|||||||
"labelIsDefault": "Default Ante",
|
"labelIsDefault": "Default Ante",
|
||||||
"placeholderName": "Please enter name",
|
"placeholderName": "Please enter name",
|
||||||
"placeholderTitle": "Please enter title",
|
"placeholderTitle": "Please enter title",
|
||||||
|
"placeholderNameAuto": "Auto from multiplier, e.g. x5",
|
||||||
|
"placeholderTitleAuto": "Auto from multiplier, e.g. x5",
|
||||||
"ruleNameRequired": "Please enter name",
|
"ruleNameRequired": "Please enter name",
|
||||||
"ruleTitleRequired": "Please enter title",
|
"ruleTitleRequired": "Please enter title",
|
||||||
"ruleMultRequired": "Please enter ante multiplier",
|
"ruleMultRequired": "Please enter ante multiplier",
|
||||||
|
|||||||
@@ -38,8 +38,31 @@
|
|||||||
"ruleGameNameRequired": "Chinese name is required",
|
"ruleGameNameRequired": "Chinese name is required",
|
||||||
"ruleGameTypeRequired": "Game type is required"
|
"ruleGameTypeRequired": "Game type is required"
|
||||||
},
|
},
|
||||||
"table": {
|
"search": {
|
||||||
|
"providerCode": "Provider Code",
|
||||||
|
"placeholderProviderCode": "Enter provider code",
|
||||||
|
"gameCode": "Game Code",
|
||||||
|
"placeholderGameCode": "Enter game code",
|
||||||
|
"gameType": "Game Type",
|
||||||
|
"placeholderGameType": "Enter game type",
|
||||||
|
"status": "Status",
|
||||||
|
"placeholderStatus": "Select status",
|
||||||
"statusEnabled": "Enabled",
|
"statusEnabled": "Enabled",
|
||||||
"statusDisabled": "Disabled"
|
"statusDisabled": "Disabled"
|
||||||
|
},
|
||||||
|
"table": {
|
||||||
|
"id": "ID",
|
||||||
|
"provider": "Provider",
|
||||||
|
"providerCode": "Provider Code",
|
||||||
|
"gameCode": "Game Code",
|
||||||
|
"gameKey": "Game Key",
|
||||||
|
"gameName": "Name (ZH)",
|
||||||
|
"gameNameEn": "Name (EN)",
|
||||||
|
"gameType": "Type",
|
||||||
|
"sort": "Sort",
|
||||||
|
"status": "Status",
|
||||||
|
"statusEnabled": "Enabled",
|
||||||
|
"statusDisabled": "Disabled",
|
||||||
|
"updateTime": "Update Time"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,10 +5,13 @@
|
|||||||
"dialogTitleEdit": "Edit Lottery Pool Config",
|
"dialogTitleEdit": "Edit Lottery Pool Config",
|
||||||
"placeholderName": "Please enter name",
|
"placeholderName": "Please enter name",
|
||||||
"placeholderRemark": "Please enter remark",
|
"placeholderRemark": "Please enter remark",
|
||||||
|
"placeholderPoolName": "Pool display name, e.g. Normal pool",
|
||||||
|
"placeholderConfigNote": "Optional notes about this pool",
|
||||||
"poolType": "Pool Type",
|
"poolType": "Pool Type",
|
||||||
"placeholderPoolType": "Please select pool type",
|
"placeholderPoolType": "Please select pool type",
|
||||||
"poolTypeNormal": "Normal",
|
"poolTypeNormal": "Normal",
|
||||||
"poolTypeKill": "Kill",
|
"poolTypeFree": "Free",
|
||||||
|
"poolTypeKill": "Force score kill",
|
||||||
"poolTypeT1": "T1 High",
|
"poolTypeT1": "T1 High",
|
||||||
"safetyLine": "Safety Line",
|
"safetyLine": "Safety Line",
|
||||||
"t1Weight": "T1 Pool Weight (%)",
|
"t1Weight": "T1 Pool Weight (%)",
|
||||||
@@ -21,20 +24,22 @@
|
|||||||
"currentPoolTitle": "Current Lottery Pool",
|
"currentPoolTitle": "Current Lottery Pool",
|
||||||
"loading": "Loading...",
|
"loading": "Loading...",
|
||||||
"poolName": "Pool Name",
|
"poolName": "Pool Name",
|
||||||
"playerProfit": "Player Total Profit (profit_amount):",
|
"poolProfitAmount": "Pool cumulative profit (profit_amount):",
|
||||||
"realtime": "Live",
|
"realtime": "Live",
|
||||||
"profitCalcHint": "Profit per round: paid = win_coin (incl. BIGWIN) - paid_amount (= ante×1); free = win_coin. Refreshes every 2s while open.",
|
"profitCalcHint": "Accumulated on name=default (Normal) pool: paid += win_coin − paid_amount (ante×1); free += win_coin. Compared with safety line to decide paid-draw kill switch. Refreshes every 2s while open.",
|
||||||
"tierRuleTitle": "Tier Rule",
|
"tierRuleTitle": "Paid draw tier rule",
|
||||||
"tierRuleContent": "When player profit in this pool is below safety line, use player T*_weight; when above or equal, use pool T*_weight (kill).",
|
"tierRuleContent": "Compares default pool profit_amount (not per-player profit). Below safety line or kill off: paid uses player T*_weight; at/above safety line with kill on: paid uses killScore pool. Free draws always use channel name=free pool weights (fallback default if missing); safety line N/A.",
|
||||||
"enableKillScore": "Enable kill score",
|
"enableKillScore": "Enable kill score",
|
||||||
"killScoreWeights": "Kill weights",
|
"killScoreWeights": "Kill weights (killScore)",
|
||||||
"killWeightNote": "(Kill weights from pool config type=1; edit in list.)",
|
"killWeightNote": "Edit killScore (Force Kill) row in the list for kill weights. This dialog only configures default pool safety line and kill switch.",
|
||||||
"btnResetProfit": "Reset Player Total Profit",
|
"btnResetProfit": "Reset pool cumulative profit",
|
||||||
"btnSaveSafetyLine": "Save Safety Line",
|
"btnSaveSafetyLine": "Save safety line & kill switch",
|
||||||
|
"safetyLineDefaultOnlyHint": "Only the default (Normal) pool safety line affects kill logic; do not set safety line on other pool types.",
|
||||||
|
"safetyLineNotUsedReadonly": "This pool type does not use safety line for kill logic. Edit the Normal (default) row or use View Current Pool.",
|
||||||
"ruleSafetyLineRequired": "Please enter safety line",
|
"ruleSafetyLineRequired": "Please enter safety line",
|
||||||
"msgGetPoolFailed": "Failed to get lottery pool",
|
"msgGetPoolFailed": "Failed to get lottery pool",
|
||||||
"msgSaveSuccess": "Save Success",
|
"msgSaveSuccess": "Save Success",
|
||||||
"msgResetProfitSuccess": "Player total profit reset to 0",
|
"msgResetProfitSuccess": "Pool cumulative profit reset to 0",
|
||||||
"msgResetFailed": "Reset failed",
|
"msgResetFailed": "Reset failed",
|
||||||
"ruleNameRequired": "Name is required",
|
"ruleNameRequired": "Name is required",
|
||||||
"rulePoolTypeRequired": "Please select pool type",
|
"rulePoolTypeRequired": "Please select pool type",
|
||||||
@@ -55,13 +60,18 @@
|
|||||||
"placeholderName": "Please enter name",
|
"placeholderName": "Please enter name",
|
||||||
"placeholderPoolType": "Please select pool type",
|
"placeholderPoolType": "Please select pool type",
|
||||||
"poolTypeNormal": "Normal",
|
"poolTypeNormal": "Normal",
|
||||||
"poolTypeKill": "Force Kill",
|
"poolTypeFree": "Free",
|
||||||
|
"poolTypeKill": "Force Score Kill",
|
||||||
"poolTypeT1": "T1 High Rate"
|
"poolTypeT1": "T1 High Rate"
|
||||||
},
|
},
|
||||||
"table": {
|
"table": {
|
||||||
"name": "Name",
|
"name": "Code",
|
||||||
|
"poolName": "Pool Name",
|
||||||
|
"configNote": "Remark",
|
||||||
"poolType": "Pool Type",
|
"poolType": "Pool Type",
|
||||||
"safetyLine": "Safety Line",
|
"safetyLine": "Safety Line",
|
||||||
|
"safetyLineNotUsed": "Not used for kill",
|
||||||
|
"safetyLineTip": "Normal (default) row only",
|
||||||
"t1PoolWeight": "T1 Pool Weight",
|
"t1PoolWeight": "T1 Pool Weight",
|
||||||
"t2PoolWeight": "T2 Pool Weight",
|
"t2PoolWeight": "T2 Pool Weight",
|
||||||
"t3PoolWeight": "T3 Pool Weight",
|
"t3PoolWeight": "T3 Pool Weight",
|
||||||
|
|||||||
@@ -57,6 +57,7 @@
|
|||||||
"rollNumber": "Roll Number",
|
"rollNumber": "Roll Number",
|
||||||
"rewardTier": "Reward Tier",
|
"rewardTier": "Reward Tier",
|
||||||
"rewardConfig": "Reward Config",
|
"rewardConfig": "Reward Config",
|
||||||
|
"createTime": "Created At",
|
||||||
"usernameFuzzy": "Username (fuzzy)",
|
"usernameFuzzy": "Username (fuzzy)",
|
||||||
"nameFuzzy": "Name (fuzzy)",
|
"nameFuzzy": "Name (fuzzy)",
|
||||||
"uiTextFuzzy": "UI Text (fuzzy)",
|
"uiTextFuzzy": "UI Text (fuzzy)",
|
||||||
@@ -84,6 +85,7 @@
|
|||||||
"rollArray": "Roll Array",
|
"rollArray": "Roll Array",
|
||||||
"rollNumber": "Roll Number",
|
"rollNumber": "Roll Number",
|
||||||
"rewardTier": "Reward Tier",
|
"rewardTier": "Reward Tier",
|
||||||
|
"remark": "Remark",
|
||||||
"createTime": "Create Time",
|
"createTime": "Create Time",
|
||||||
"updateTime": "Update Time"
|
"updateTime": "Update Time"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,8 @@
|
|||||||
"platformTotalProfit": "Platform Total Profit"
|
"platformTotalProfit": "Platform Total Profit"
|
||||||
},
|
},
|
||||||
"search": {
|
"search": {
|
||||||
|
"lotteryPoolConfig": "Lottery Pool Config",
|
||||||
|
"placeholderLotteryPool": "Select pool (search by name)",
|
||||||
"rewardConfigRecordId": "Weight Test Record ID",
|
"rewardConfigRecordId": "Weight Test Record ID",
|
||||||
"drawType": "Draw Type",
|
"drawType": "Draw Type",
|
||||||
"direction": "Direction",
|
"direction": "Direction",
|
||||||
|
|||||||
@@ -22,6 +22,8 @@
|
|||||||
"placeholderLotteryPool": "Leave empty for custom weights below, or select pool",
|
"placeholderLotteryPool": "Leave empty for custom weights below, or select pool",
|
||||||
"currentConfig": "Current Config",
|
"currentConfig": "Current Config",
|
||||||
"configLabelName": "Name",
|
"configLabelName": "Name",
|
||||||
|
"configLabelPoolName": "Pool name",
|
||||||
|
"configLabelCode": "Code",
|
||||||
"configLabelType": "Type",
|
"configLabelType": "Type",
|
||||||
"configLabelWeights": "T1–T5 Weights",
|
"configLabelWeights": "T1–T5 Weights",
|
||||||
"configLabelRemark": "Remark",
|
"configLabelRemark": "Remark",
|
||||||
@@ -70,7 +72,8 @@
|
|||||||
"placeholderNickname": "Please enter nickname",
|
"placeholderNickname": "Please enter nickname",
|
||||||
"placeholderPhoneFuzzy": "Phone (fuzzy)",
|
"placeholderPhoneFuzzy": "Phone (fuzzy)",
|
||||||
"placeholderAll": "All",
|
"placeholderAll": "All",
|
||||||
"exactSearch": "Exact"
|
"exactSearch": "Exact",
|
||||||
|
"createTime": "Register Time"
|
||||||
},
|
},
|
||||||
"table": {
|
"table": {
|
||||||
"username": "Username",
|
"username": "Username",
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
{
|
{
|
||||||
"toolbar": {
|
"toolbar": {
|
||||||
"coinChangeSummary": "Coin Change Summary"
|
"coinChangeSummary": "Net Coin Change",
|
||||||
|
"coinInflow": "Inflow",
|
||||||
|
"coinOutflow": "Outflow"
|
||||||
},
|
},
|
||||||
"form": {
|
"form": {
|
||||||
"dialogTitleAdd": "Add Wallet Record",
|
"dialogTitleAdd": "Add Wallet Record",
|
||||||
|
|||||||
@@ -25,8 +25,6 @@
|
|||||||
"emptyTier": "No data for this tier",
|
"emptyTier": "No data for this tier",
|
||||||
"sumLineDual": "Tier weight sum (clockwise): {cw}; counter-clockwise: {ccw} (each row 1–10000, ratio draw within tier, sum not limited)",
|
"sumLineDual": "Tier weight sum (clockwise): {cw}; counter-clockwise: {ccw} (each row 1–10000, ratio draw within tier, sum not limited)",
|
||||||
"sumLineSingle": "Tier weight sum: {sum} (each row 1–10000, ratio draw within tier, sum not limited)",
|
"sumLineSingle": "Tier weight sum: {sum} (each row 1–10000, ratio draw within tier, sum not limited)",
|
||||||
"t4t5NoteSingle": "T4 and T5 have a single outcome; no weight configuration.",
|
|
||||||
"t4t5NoteDual": "T4 and T5 have a single outcome when hit; no weight configuration.",
|
|
||||||
"colEndIndexId": "End Index (id)",
|
"colEndIndexId": "End Index (id)",
|
||||||
"colGridNumber": "Points (grid_number)",
|
"colGridNumber": "Points (grid_number)",
|
||||||
"colDicePoints": "Dice Points",
|
"colDicePoints": "Dice Points",
|
||||||
@@ -45,33 +43,40 @@
|
|||||||
},
|
},
|
||||||
"weightEdit": {
|
"weightEdit": {
|
||||||
"title": "Dice Reward (dice_reward) Weight Ratio",
|
"title": "Dice Reward (dice_reward) Weight Ratio",
|
||||||
"globalTip": "You are editing weights on dice_reward (DiceReward), split by end_index into clockwise and counter-clockwise; the draw uses the set for the current direction."
|
"globalTip": "You are editing weights on dice_reward (DiceReward), split by end_index into clockwise and counter-clockwise; the draw uses the set for the current direction. T4/T5 support the same multi-point weight ratio as T1–T3."
|
||||||
},
|
},
|
||||||
"weightRatio": {
|
"weightRatio": {
|
||||||
"title": "Weight Ratio",
|
"title": "Weight Ratio",
|
||||||
"globalTip": "Configure dice_reward weights: first by direction (clockwise / counter-clockwise), then by tier (T1–T5); each row weight 1–10000, ratio draw within tier.",
|
"globalTip": "Configure dice_reward weights: first by direction (clockwise / counter-clockwise), then by tier (T1–T5); each row weight 1–10000, ratio draw within tier for dice points (T4/T5 use the same logic as T1–T3 and support multiple point weights).",
|
||||||
"tabClockwise": "Clockwise",
|
"tabClockwise": "Clockwise",
|
||||||
"tabCounterclockwise": "Counter-clockwise"
|
"tabCounterclockwise": "Counter-clockwise"
|
||||||
},
|
},
|
||||||
"weightTest": {
|
"weightTest": {
|
||||||
"title": "One-Click Weight Test",
|
"title": "One-Click Weight Test",
|
||||||
"alertTitle": "Bonus pool logic",
|
"alertTitle": "Bonus pool logic",
|
||||||
"alertBody": "Test mode is non-kill by default. You can enable kill mode below with switch + safety line: once simulated player cumulative profit reaches the line, paid draws switch to killScore.",
|
"alertBody": "Kill switching is off by default. Enable “Test kill mode” below and set a test safety line (independent from the lottery pool config) to simulate kill triggers.",
|
||||||
"chainModeHint": "Simulation: set paid spin counts only (CW/CCW). If a paid draw hits “play again” (or T5), the next draw is free with the same ante, lottery type free, paid amount 0. Free-draw tier odds are configured below (including chained free plays).",
|
"chainModeHint": "Simulation: set paid spin counts only (CW/CCW). If a paid draw hits “play again” (or T5), the next draw is free with the same ante, lottery type free, paid amount 0. Free-draw tier odds are configured below (including chained free plays).",
|
||||||
"killModeHint": "When test kill mode is enabled: use simulated player cumulative profit as trigger; once cumulative profit >= safety line, subsequent paid draws use killScore. Free draws still follow the configured free settings.",
|
"killModeHint": "When test kill mode is on: start from default pool profit_amount and accumulate each spin (paid: win_coin - paid_amount; free: win_coin). Once profit >= the test safety line below, subsequent paid draws use killScore; free draws still use the name=free pool.",
|
||||||
"labelKillModeEnabled": "Enable test kill mode",
|
"killModePanelTitle": "Test kill mode",
|
||||||
|
"killModeSwitchOn": "On",
|
||||||
|
"killModeSwitchOff": "Off",
|
||||||
"labelTestSafetyLine": "Test safety line",
|
"labelTestSafetyLine": "Test safety line",
|
||||||
|
"testSafetyLineHint": "Used for this weight test only, independent from the pool config safety line. Use a lower value to observe kill switching quickly.",
|
||||||
|
"poolProfitRef": "Reference: default pool profit {profit}, pool config safety line {line}",
|
||||||
|
"killModeOffHint": "When off, all draws follow paid/free settings without kill switching.",
|
||||||
"sectionPaid": "Paid draws",
|
"sectionPaid": "Paid draws",
|
||||||
"sectionFreeAfterPlayAgain": "Free draw tier odds (after play-again)",
|
"sectionFreeAfterPlayAgain": "Free draws (play again)",
|
||||||
"tierProbHintFreeChain": "When using custom tier odds: T1–T5 below apply when a free draw runs (tier roll; combined with dice_reward row weights).",
|
"tierProbHintFreeChain": "Custom tiers: T1–T5 odds for free draws (combined with dice_reward row weights).",
|
||||||
"stepPaid": "Paid ticket",
|
"stepPaid": "Paid ticket",
|
||||||
"stepFree": "Free ticket",
|
"stepFree": "Free ticket",
|
||||||
"labelLotteryTypePaid": "Test pool type",
|
"labelLotteryTypePaid": "Paid tier pool",
|
||||||
"labelLotteryTypeFree": "Test pool type",
|
"labelLotteryTypeFree": "Free tier pool",
|
||||||
"labelAnte": "Ante",
|
"labelAnte": "Ante",
|
||||||
"placeholderAnte": "Select ante config",
|
"placeholderAnte": "Select ante config",
|
||||||
"placeholderPaidPool": "Leave empty for custom tier odds below (default: default)",
|
"anteRandomOption": "Random (each paid spin picks independently from channel ante configs)",
|
||||||
"placeholderFreePool": "Leave empty for custom tier odds below (default: killScore)",
|
"placeholderPaidPool": "Leave empty to set T1–T5 weights manually",
|
||||||
|
"placeholderFreePool": "Leave empty to set T1–T5 weights manually",
|
||||||
|
"selectedPoolHint": "Selected pool: {name}",
|
||||||
"tierProbHint": "Custom tier odds (T1–T5), each 0–100%, sum of five must not exceed 100%",
|
"tierProbHint": "Custom tier odds (T1–T5), each 0–100%, sum of five must not exceed 100%",
|
||||||
"tierFieldLabel": "Tier {tier} (%)",
|
"tierFieldLabel": "Tier {tier} (%)",
|
||||||
"tierSumError": "Current sum of five tiers is {sum}%, cannot exceed 100%",
|
"tierSumError": "Current sum of five tiers is {sum}%, cannot exceed 100%",
|
||||||
|
|||||||
@@ -8,6 +8,18 @@
|
|||||||
"tabIndex": "Reward Index",
|
"tabIndex": "Reward Index",
|
||||||
"tabBigwin": "Big Win Weights",
|
"tabBigwin": "Big Win Weights",
|
||||||
"tipIndex": "Dice points must be between 5 and 30 and unique in this table.",
|
"tipIndex": "Dice points must be between 5 and 30 and unique in this table.",
|
||||||
|
"tierRecommendRules": "[Settlement vs tier] T1 (big prize): >2; T2 (small win): 2≥amount>1; T3 (rake): 1≥amount>0; T4 (penalty): 0>amount; T5 (try again): 0=amount. Set recommended settlement per tier below. The Tier column is auto-calculated from settlement and cannot be edited manually.",
|
||||||
|
"tierRecommendRealEv": "Recommended settlement",
|
||||||
|
"tierRecommendAutoMatch": "Auto-match tier when settlement changes",
|
||||||
|
"tierRecommendApplyAmount": "Fill recommended amount for rows with tier set",
|
||||||
|
"tierRecommendApplyAmountOk": "Filled recommended settlement for {n} row(s)",
|
||||||
|
"tierRecommendNoTierRows": "No rows with a tier inferable from settlement",
|
||||||
|
"tierRecommendMatchTier": "Match all tiers from settlement",
|
||||||
|
"tierRecommendMatchTierOk": "Matched tier for {n} row(s) from settlement",
|
||||||
|
"tierRecommendMatchTierNone": "No rows to match",
|
||||||
|
"tierRecommendT5UiText": "再来一次",
|
||||||
|
"tierRecommendT5UiTextEn": "Once again",
|
||||||
|
"colTierAutoHint": "Auto-matched from settlement",
|
||||||
"tipBigwin": "Left to right: big-win points (read-only), display text, real EV, remark, weight (0~10000). Points 5 and 30 are fixed at 100%. This tab saves big-win weights only.",
|
"tipBigwin": "Left to right: big-win points (read-only), display text, real EV, remark, weight (0~10000). Points 5 and 30 are fixed at 100%. This tab saves big-win weights only.",
|
||||||
"colId": "Index (id)",
|
"colId": "Index (id)",
|
||||||
"colDicePoints": "Dice Points",
|
"colDicePoints": "Dice Points",
|
||||||
@@ -36,6 +48,19 @@
|
|||||||
"confirmCreateRefMsg": "Create reward reference by rule: start_index is the id of the cell for grid_number in reward config; clockwise end_index=(start_index+roll)%26; counter-clockwise end_index=start_index-roll if >=0 else 26+start_index-roll. Existing data will be cleared, then 26 points (5–30) for both directions will be generated. Continue?",
|
"confirmCreateRefMsg": "Create reward reference by rule: start_index is the id of the cell for grid_number in reward config; clockwise end_index=(start_index+roll)%26; counter-clockwise end_index=start_index-roll if >=0 else 26+start_index-roll. Existing data will be cleared, then 26 points (5–30) for both directions will be generated. Continue?",
|
||||||
"confirmCreateRefOk": "Create",
|
"confirmCreateRefOk": "Create",
|
||||||
"confirmCreateRefCancel": "Cancel",
|
"confirmCreateRefCancel": "Cancel",
|
||||||
|
"createRefPreviewTitle": "Create Reward Reference Preview",
|
||||||
|
"createRefPreviewClockwise": "Clockwise",
|
||||||
|
"createRefPreviewCounterclockwise": "Counter-clockwise",
|
||||||
|
"createRefPreviewTipUnchanged": "Dice points mapping is unchanged: weights in the preview are reused from current dice_reward; importing will not override existing weights.",
|
||||||
|
"createRefPreviewTipChanged": "Dice points mapping has changed: preview weights use defaults (100 for normal sums; 10 for sums 10/15/20/25; 1 for 5/30). After importing, adjust weights in the Dice Reward page if needed.",
|
||||||
|
"createRefPreviewSkipped": "{n} dice point(s) are missing in the reward index and were skipped (please complete all 26 points from 5 to 30).",
|
||||||
|
"createRefPreviewRefresh": "Refresh preview",
|
||||||
|
"createRefPreviewImport": "Import",
|
||||||
|
"createRefPreviewImportOk": "Imported reward reference",
|
||||||
|
"createRefPreviewImportNoop": "Mapping unchanged, nothing to import (existing weights kept)",
|
||||||
|
"createRefPreviewDiff": "Diff (old → new)",
|
||||||
|
"createRefPreviewNoDiff": "No change",
|
||||||
|
"createRefPreviewWeightsSaved": "Weights saved",
|
||||||
"createRefSuccess": "Created for 26 dice points (5–30), clockwise + counter-clockwise: clockwise added {cwNew}, counter-clockwise added {ccwNew}; clockwise updated {cwUp}, counter-clockwise updated {ccwUp}{skippedPart}",
|
"createRefSuccess": "Created for 26 dice points (5–30), clockwise + counter-clockwise: clockwise added {cwNew}, counter-clockwise added {ccwNew}; clockwise updated {cwUp}, counter-clockwise updated {ccwUp}{skippedPart}",
|
||||||
"createRefSuccessSkipped": "; {n} point(s) used fallback start index",
|
"createRefSuccessSkipped": "; {n} point(s) used fallback start index",
|
||||||
"createRefSuccessSimple": "Created successfully",
|
"createRefSuccessSimple": "Created successfully",
|
||||||
@@ -54,7 +79,7 @@
|
|||||||
"infoNoBigwin": "No BIGWIN rows. Set tier to BIGWIN in the Reward Index tab first.",
|
"infoNoBigwin": "No BIGWIN rows. Set tier to BIGWIN in the Reward Index tab first.",
|
||||||
"btnRuleGenerate": "Generate by rules",
|
"btnRuleGenerate": "Generate by rules",
|
||||||
"ruleGenerateTitle": "Generate reward index by rules",
|
"ruleGenerateTitle": "Generate reward index by rules",
|
||||||
"ruleGenerateRules": "[Generation logic (same as Create Reward Reference)]\n• 26 cells ordered by id ascending are positions 0–25; each row’s grid_number is 5–30 and unique.\n• Roll D (5–30): start at the cell whose grid_number equals D (start_index); clockwise landing = (start position + D) mod 26; counter-clockwise = start − D (if negative, +26).\n• Each reference row’s “dice points” column is the roll D; tier / real_ev / display text come from the config at the landing id.\n\n[Leopard rolls]\nFor rolls 5, 10, 15, 20, 25, 30, clockwise and counter-clockwise landing tiers must NOT be T4 or T5 (avoid leopard roll + penalty / once again).\n\n[Settlement amount vs tier]\nSettlement < 0 → T4; 0 < Settlement < 100 → T3; 100 < Settlement < 200 → T2; Settlement > 200 → T1; T5 “once again” settlement = 0. You can set a unified settlement standard for each tier below; generated rows write those values into the config, and details can be edited later in the table.\n\n[Inputs in this dialog]\nCount: T1/T4/T5 are fixed; T2 is minimum. Clockwise and counter-clockwise weighted counts (each roll result counts once) must each satisfy the entered values; T1, T4, and T5 are entered separately.\nSettlement standard: all cells in the same tier use the same value. On generation, T1–T4 use ui_text / ui_text_en = settlement real_ev; T5 is fixed to \"再来一次\" / \"Once again\". Remarks still distinguish break-even / small win, etc.",
|
"ruleGenerateRules": "[Generation logic (same as Create Reward Reference)]\n• 26 cells ordered by id ascending are positions 0–25; each row’s grid_number is 5–30 and unique.\n• Roll D (5–30): start at the cell whose grid_number equals D (start_index); clockwise landing = (start position + D) mod 26; counter-clockwise = start − D (if negative, +26).\n• Each reference row’s “dice points” column is the roll D; tier / real_ev / display text come from the config at the landing id.\n\n[Leopard rolls]\nFor rolls 5, 10, 15, 20, 25, 30, clockwise and counter-clockwise landing tiers must NOT be T4 or T5 (avoid leopard roll + penalty / try again).\n\n[Settlement vs tier]\nT1: >2; T2: 2≥amount>1; T3: 1≥amount>0; T4: 0>amount; T5: 0=amount. Set recommended settlement per tier below.\n\n[Inputs in this dialog]\nCount: T1/T4/T5 are fixed; T2 is minimum. Clockwise and counter-clockwise weighted counts must each satisfy the entered values.\nSettlement standard: same tier uses the same value. T1–T4 use ui_text = settlement; T5 is fixed to \"再来一次\" / \"Once again\".",
|
||||||
"ruleGenT1Row": "T1 (big prize)",
|
"ruleGenT1Row": "T1 (big prize)",
|
||||||
"ruleGenT2Row": "T2 (small win / break-even)",
|
"ruleGenT2Row": "T2 (small win / break-even)",
|
||||||
"ruleGenT3RealEvOnly": "T3 (rake)",
|
"ruleGenT3RealEvOnly": "T3 (rake)",
|
||||||
@@ -64,11 +89,11 @@
|
|||||||
"ruleGenFixedCount": "Fixed count (CW & CCW)",
|
"ruleGenFixedCount": "Fixed count (CW & CCW)",
|
||||||
"ruleGenRealEvStd": "real_ev standard",
|
"ruleGenRealEvStd": "real_ev standard",
|
||||||
"ruleGenRealEvEditHint": "After saving, you can still edit display text, EN, real_ev and remarks per row in the table above.",
|
"ruleGenRealEvEditHint": "After saving, you can still edit display text, EN, real_ev and remarks per row in the table above.",
|
||||||
"ruleGenInvalidT1RealEv": "T1 settlement amount must satisfy: value > 200",
|
"ruleGenInvalidT1RealEv": "T1 (big prize) settlement must satisfy: value > 2",
|
||||||
"ruleGenInvalidT2RealEv": "T2 settlement amount must satisfy: 100 < value < 200",
|
"ruleGenInvalidT2RealEv": "T2 (small win) settlement must satisfy: 1 < value ≤ 2",
|
||||||
"ruleGenInvalidT3RealEv": "T3 settlement amount must satisfy: 0 < value < 100",
|
"ruleGenInvalidT3RealEv": "T3 (rake) settlement must satisfy: 0 < value ≤ 1",
|
||||||
"ruleGenInvalidT4RealEv": "T4 settlement amount must satisfy: value < 0",
|
"ruleGenInvalidT4RealEv": "T4 (penalty) settlement must satisfy: value < 0",
|
||||||
"ruleGenInvalidT5RealEv": "T5 “try again” real_ev must be 0",
|
"ruleGenInvalidT5RealEv": "T5 (try again) settlement must be 0",
|
||||||
"ruleGenT1Min": "T1 fixed count (CW & CCW)",
|
"ruleGenT1Min": "T1 fixed count (CW & CCW)",
|
||||||
"ruleGenT2Min": "T2 min (CW & CCW)",
|
"ruleGenT2Min": "T2 min (CW & CCW)",
|
||||||
"ruleGenT4Max": "T4 fixed count (CW & CCW)",
|
"ruleGenT4Max": "T4 fixed count (CW & CCW)",
|
||||||
|
|||||||
@@ -17,6 +17,8 @@
|
|||||||
"chainModeNo": "No",
|
"chainModeNo": "No",
|
||||||
"paidPlannedSpins": "Planned paid spins",
|
"paidPlannedSpins": "Planned paid spins",
|
||||||
"ante": "Ante",
|
"ante": "Ante",
|
||||||
|
"anteRandom": "Random",
|
||||||
|
"testSafetyLine": "Safety line",
|
||||||
"playAgainCount": "Play-again count",
|
"playAgainCount": "Play-again count",
|
||||||
"progressDraws": "{over} done",
|
"progressDraws": "{over} done",
|
||||||
"progressFailed": "{over} before fail",
|
"progressFailed": "{over} before fail",
|
||||||
@@ -51,11 +53,13 @@
|
|||||||
"testCountProgress": "In progress: {over} done",
|
"testCountProgress": "In progress: {over} done",
|
||||||
"testCountFailed": "{over} before failure",
|
"testCountFailed": "{over} before failure",
|
||||||
"chainModeLabel": "Chain play-again",
|
"chainModeLabel": "Chain play-again",
|
||||||
|
"killModeOff": "Kill mode off",
|
||||||
"paidPlannedSpins": "Planned paid spins",
|
"paidPlannedSpins": "Planned paid spins",
|
||||||
|
"testSafetyLine": "Test safety line",
|
||||||
"createTime": "Created at",
|
"createTime": "Created at",
|
||||||
"admin": "Operator",
|
"admin": "Operator",
|
||||||
"paidPoolId": "Paid lottery pool config ID",
|
"paidPoolId": "Paid lottery pool",
|
||||||
"freePoolId": "Free lottery pool config ID",
|
"freePoolId": "Free lottery pool",
|
||||||
"bigwinSnapshot": "BIGWIN weight snapshot",
|
"bigwinSnapshot": "BIGWIN weight snapshot",
|
||||||
"sectionPaidTier": "Paid draw tier odds (T1–T5, used in test)",
|
"sectionPaidTier": "Paid draw tier odds (T1–T5, used in test)",
|
||||||
"sectionFreeTier": "Free draw tier odds (T1–T5, used in test)",
|
"sectionFreeTier": "Free draw tier odds (T1–T5, used in test)",
|
||||||
|
|||||||
27
saiadmin-artd/src/locales/langs/en/system/admin_guide.json
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"title": "Admin Guide",
|
||||||
|
"toolbar": {
|
||||||
|
"edit": "Edit",
|
||||||
|
"save": "Save",
|
||||||
|
"cancel": "Cancel",
|
||||||
|
"refresh": "Refresh"
|
||||||
|
},
|
||||||
|
"meta": {
|
||||||
|
"filePath": "File Path",
|
||||||
|
"updateTime": "Updated At"
|
||||||
|
},
|
||||||
|
"catalog": {
|
||||||
|
"title": "Catalog",
|
||||||
|
"empty": "No headings"
|
||||||
|
},
|
||||||
|
"image": {
|
||||||
|
"zoom": "Click to view full size"
|
||||||
|
},
|
||||||
|
"message": {
|
||||||
|
"loadFailed": "Failed to load admin guide",
|
||||||
|
"saveSuccess": "Saved successfully",
|
||||||
|
"saveFailed": "Failed to save",
|
||||||
|
"cancelConfirm": "You have unsaved changes. Cancel editing?",
|
||||||
|
"editRequired": "Please click Edit before saving"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -42,6 +42,7 @@
|
|||||||
"listTitle": "渠道列表",
|
"listTitle": "渠道列表",
|
||||||
"defaultTemplate": "默认配置模板",
|
"defaultTemplate": "默认配置模板",
|
||||||
"defaultRoleTemplate": "默认角色模板",
|
"defaultRoleTemplate": "默认角色模板",
|
||||||
|
"allChannels": "全部",
|
||||||
"currentConfig": "当前配置",
|
"currentConfig": "当前配置",
|
||||||
"currentChannel": "当前渠道",
|
"currentChannel": "当前渠道",
|
||||||
"currentRole": "当前角色范围"
|
"currentRole": "当前角色范围"
|
||||||
@@ -299,16 +300,33 @@
|
|||||||
"gohome": "返回首页"
|
"gohome": "返回首页"
|
||||||
},
|
},
|
||||||
"console": {
|
"console": {
|
||||||
|
"filter": {
|
||||||
|
"date": "统计日期",
|
||||||
|
"placeholder": "选择日期",
|
||||||
|
"today": "今日",
|
||||||
|
"clear": "清空"
|
||||||
|
},
|
||||||
"card": {
|
"card": {
|
||||||
"playerRegister": "玩家注册",
|
"playerRegister": "玩家注册",
|
||||||
"playerCharge": "玩家充值",
|
"playerCharge": "玩家充值",
|
||||||
"playerWithdraw": "玩家提现",
|
"playerWithdraw": "玩家提现",
|
||||||
"playerPlayCount": "玩家游玩次数",
|
"playerPlayCount": "玩家游玩次数",
|
||||||
"vsLastWeek": "较上周"
|
"vsLastWeek": "较上周",
|
||||||
|
"vsYesterday": "较昨日",
|
||||||
|
"viewRechargeRecords": "查看充值记录",
|
||||||
|
"viewWithdrawRecords": "查看提现记录",
|
||||||
|
"viewPlayRecords": "查看游玩记录",
|
||||||
|
"viewRegisterRecords": "查看注册玩家"
|
||||||
|
},
|
||||||
|
"nav": {
|
||||||
|
"viewAllRecharge": "查看全部充值",
|
||||||
|
"viewAllPlay": "查看全部游玩",
|
||||||
|
"viewAllRegister": "查看全部注册"
|
||||||
},
|
},
|
||||||
"newPlayer": {
|
"newPlayer": {
|
||||||
"title": "新增玩家",
|
"title": "新增玩家",
|
||||||
"subtitle": "最新50条新增玩家记录",
|
"subtitle": "最新50条新增玩家记录",
|
||||||
|
"subtitleByDate": "{date} 新增玩家记录(最多50条)",
|
||||||
"player": "玩家",
|
"player": "玩家",
|
||||||
"balance": "余额",
|
"balance": "余额",
|
||||||
"ticket": "抽奖券",
|
"ticket": "抽奖券",
|
||||||
@@ -317,6 +335,7 @@
|
|||||||
"playRecord": {
|
"playRecord": {
|
||||||
"title": "玩家游玩记录",
|
"title": "玩家游玩记录",
|
||||||
"subtitle": "最新50条游玩记录",
|
"subtitle": "最新50条游玩记录",
|
||||||
|
"subtitleByDate": "{date} 游玩记录(最多50条)",
|
||||||
"player": "玩家",
|
"player": "玩家",
|
||||||
"reward": "中奖档位",
|
"reward": "中奖档位",
|
||||||
"winCoin": "赢取平台币",
|
"winCoin": "赢取平台币",
|
||||||
@@ -325,6 +344,7 @@
|
|||||||
"walletRecord": {
|
"walletRecord": {
|
||||||
"title": "玩家充值记录",
|
"title": "玩家充值记录",
|
||||||
"subtitle": "最新50条充值记录",
|
"subtitle": "最新50条充值记录",
|
||||||
|
"subtitleByDate": "{date} 充值记录(最多50条)",
|
||||||
"player": "玩家",
|
"player": "玩家",
|
||||||
"chargeAmount": "充值金额",
|
"chargeAmount": "充值金额",
|
||||||
"chargeTime": "充值时间"
|
"chargeTime": "充值时间"
|
||||||
@@ -423,6 +443,9 @@
|
|||||||
"rewardConfigRecord": "权重测试记录",
|
"rewardConfigRecord": "权重测试记录",
|
||||||
"playRecordTest": "抽奖记录(测试权重)",
|
"playRecordTest": "抽奖记录(测试权重)",
|
||||||
"config": "游戏配置"
|
"config": "游戏配置"
|
||||||
|
},
|
||||||
|
"game": {
|
||||||
|
"title": "游戏管理"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"table": {
|
"table": {
|
||||||
@@ -442,6 +465,10 @@
|
|||||||
"max": "最大",
|
"max": "最大",
|
||||||
"startTime": "开始时间",
|
"startTime": "开始时间",
|
||||||
"endTime": "结束时间",
|
"endTime": "结束时间",
|
||||||
|
"quickDate": "快捷日期",
|
||||||
|
"quickToday": "今日",
|
||||||
|
"quickYesterday": "昨日",
|
||||||
|
"quickLast7Days": "近7天",
|
||||||
"placeholderUsername": "请输入用户名",
|
"placeholderUsername": "请输入用户名",
|
||||||
"placeholderNickname": "请输入昵称",
|
"placeholderNickname": "请输入昵称",
|
||||||
"placeholderPhone": "请输入手机号",
|
"placeholderPhone": "请输入手机号",
|
||||||
|
|||||||
@@ -27,6 +27,8 @@
|
|||||||
"labelIsDefault": "默认底注",
|
"labelIsDefault": "默认底注",
|
||||||
"placeholderName": "请输入名称",
|
"placeholderName": "请输入名称",
|
||||||
"placeholderTitle": "请输入标题",
|
"placeholderTitle": "请输入标题",
|
||||||
|
"placeholderNameAuto": "随底注倍率自动生成,如 x5",
|
||||||
|
"placeholderTitleAuto": "随底注倍率自动生成,如 x5",
|
||||||
"ruleNameRequired": "请输入名称",
|
"ruleNameRequired": "请输入名称",
|
||||||
"ruleTitleRequired": "请输入标题",
|
"ruleTitleRequired": "请输入标题",
|
||||||
"ruleMultRequired": "请输入底注倍率",
|
"ruleMultRequired": "请输入底注倍率",
|
||||||
|
|||||||
@@ -38,8 +38,31 @@
|
|||||||
"ruleGameNameRequired": "请输入中文名称",
|
"ruleGameNameRequired": "请输入中文名称",
|
||||||
"ruleGameTypeRequired": "请输入游戏类型"
|
"ruleGameTypeRequired": "请输入游戏类型"
|
||||||
},
|
},
|
||||||
"table": {
|
"search": {
|
||||||
|
"providerCode": "供应商编码",
|
||||||
|
"placeholderProviderCode": "请输入供应商编码",
|
||||||
|
"gameCode": "游戏编号",
|
||||||
|
"placeholderGameCode": "请输入游戏编号",
|
||||||
|
"gameType": "游戏类型",
|
||||||
|
"placeholderGameType": "请输入游戏类型",
|
||||||
|
"status": "状态",
|
||||||
|
"placeholderStatus": "请选择状态",
|
||||||
"statusEnabled": "启用",
|
"statusEnabled": "启用",
|
||||||
"statusDisabled": "禁用"
|
"statusDisabled": "禁用"
|
||||||
|
},
|
||||||
|
"table": {
|
||||||
|
"id": "ID",
|
||||||
|
"provider": "供应商",
|
||||||
|
"providerCode": "供应商编码",
|
||||||
|
"gameCode": "游戏编号",
|
||||||
|
"gameKey": "游戏唯一值",
|
||||||
|
"gameName": "中文名",
|
||||||
|
"gameNameEn": "英文名",
|
||||||
|
"gameType": "类型",
|
||||||
|
"sort": "排序",
|
||||||
|
"status": "状态",
|
||||||
|
"statusEnabled": "启用",
|
||||||
|
"statusDisabled": "禁用",
|
||||||
|
"updateTime": "更新时间"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,10 +5,14 @@
|
|||||||
"dialogTitleEdit": "编辑色子奖池配置",
|
"dialogTitleEdit": "编辑色子奖池配置",
|
||||||
"placeholderName": "请输入名称",
|
"placeholderName": "请输入名称",
|
||||||
"placeholderRemark": "请输入备注",
|
"placeholderRemark": "请输入备注",
|
||||||
|
"placeholderPoolName": "请输入奖池名称,如:正常池",
|
||||||
|
"placeholderConfigNote": "选填,用于说明该奖池用途或规则",
|
||||||
"poolType": "奖池类型",
|
"poolType": "奖池类型",
|
||||||
|
"poolName": "奖池名称",
|
||||||
"placeholderPoolType": "请选择奖池类型",
|
"placeholderPoolType": "请选择奖池类型",
|
||||||
"poolTypeNormal": "正常",
|
"poolTypeNormal": "正常",
|
||||||
"poolTypeKill": "强制杀猪",
|
"poolTypeFree": "免费",
|
||||||
|
"poolTypeKill": "强制杀分",
|
||||||
"poolTypeT1": "T1高倍率",
|
"poolTypeT1": "T1高倍率",
|
||||||
"safetyLine": "安全线",
|
"safetyLine": "安全线",
|
||||||
"t1Weight": "T1池权重(%)",
|
"t1Weight": "T1池权重(%)",
|
||||||
@@ -21,20 +25,22 @@
|
|||||||
"currentPoolTitle": "当前彩金池",
|
"currentPoolTitle": "当前彩金池",
|
||||||
"loading": "加载中...",
|
"loading": "加载中...",
|
||||||
"poolName": "池子名称",
|
"poolName": "池子名称",
|
||||||
"playerProfit": "玩家累计盈利(profit_amount):",
|
"poolProfitAmount": "彩金池累计盈利(profit_amount):",
|
||||||
"realtime": "实时",
|
"realtime": "实时",
|
||||||
"profitCalcHint": "计算方式:付费每局按“赢取平台币 win_coin(含 BIGWIN)减去付费金额 压注金额paid_amount(= 压注倍数ante×1)”累加;免费每局按“玩家赢得平台币win_coin”累加。弹窗打开期间每 2 秒自动刷新",
|
"profitCalcHint": "累计在 name=default(正常)奖池上:付费每局 += win_coin − paid_amount(ante×1);免费每局 += win_coin。用于与安全线比较,判定付费抽奖是否切换杀分。弹窗打开期间每 2 秒自动刷新。",
|
||||||
"tierRuleTitle": "抽奖档位规则",
|
"tierRuleTitle": "付费抽奖档位规则",
|
||||||
"tierRuleContent": "当玩家在当前彩金池的累计盈利 低于安全线 时,按 玩家 的 T*_weight 权重抽取档位;当累计盈利 高于或等于安全线 时,按 当前彩金池 的 T*_weight 权重抽取档位(杀分)。",
|
"tierRuleContent": "比较对象为 default 奖池的 profit_amount(非单个玩家盈利)。当 profit_amount 低于安全线或未开启杀分时,付费按玩家 T*_weight 抽档;当 profit_amount 高于或等于安全线且已开启杀分时,付费按 killScore 奖池抽档。免费抽奖始终按本渠道 name=free 奖池权重(无 free 时回退 default),与安全线无关。",
|
||||||
"enableKillScore": "开启杀分",
|
"enableKillScore": "开启杀分",
|
||||||
"killScoreWeights": "杀分权重",
|
"killScoreWeights": "杀分权重(killScore)",
|
||||||
"killWeightNote": "(杀分权重来自奖池配置,请在列表中编辑对应记录)",
|
"killWeightNote": "杀分权重请在列表中编辑 name=killScore(强制杀分)记录;本弹窗仅配置 default 奖池的安全线与杀分开关。",
|
||||||
"btnResetProfit": "重置玩家累计盈利",
|
"btnResetProfit": "重置彩金池累计盈利",
|
||||||
"btnSaveSafetyLine": "保存安全线",
|
"btnSaveSafetyLine": "保存安全线与杀分开关",
|
||||||
|
"safetyLineDefaultOnlyHint": "仅 name=default(正常)奖池的安全线参与杀分判定;其它奖池类型请勿在此配置安全线。",
|
||||||
|
"safetyLineNotUsedReadonly": "当前奖池类型不参与杀分判定,安全线仅对「正常(default)」奖池生效,请通过「查看当前彩金池」或编辑正常行修改。",
|
||||||
"ruleSafetyLineRequired": "请输入安全线",
|
"ruleSafetyLineRequired": "请输入安全线",
|
||||||
"msgGetPoolFailed": "获取彩金池失败",
|
"msgGetPoolFailed": "获取彩金池失败",
|
||||||
"msgSaveSuccess": "保存成功",
|
"msgSaveSuccess": "保存成功",
|
||||||
"msgResetProfitSuccess": "玩家累计盈利已重置为 0",
|
"msgResetProfitSuccess": "彩金池累计盈利已重置为 0",
|
||||||
"msgResetFailed": "重置失败",
|
"msgResetFailed": "重置失败",
|
||||||
"ruleNameRequired": "名称必需填写",
|
"ruleNameRequired": "名称必需填写",
|
||||||
"rulePoolTypeRequired": "请选择奖池类型",
|
"rulePoolTypeRequired": "请选择奖池类型",
|
||||||
@@ -52,16 +58,22 @@
|
|||||||
},
|
},
|
||||||
"search": {
|
"search": {
|
||||||
"poolType": "奖池类型",
|
"poolType": "奖池类型",
|
||||||
|
"poolName": "奖池名称",
|
||||||
"placeholderName": "请输入名称",
|
"placeholderName": "请输入名称",
|
||||||
"placeholderPoolType": "请选择奖池类型",
|
"placeholderPoolType": "请选择奖池类型",
|
||||||
"poolTypeNormal": "正常",
|
"poolTypeNormal": "正常",
|
||||||
"poolTypeKill": "强制杀猪",
|
"poolTypeFree": "免费",
|
||||||
|
"poolTypeKill": "强制杀分",
|
||||||
"poolTypeT1": "T1高倍率"
|
"poolTypeT1": "T1高倍率"
|
||||||
},
|
},
|
||||||
"table": {
|
"table": {
|
||||||
"name": "名称",
|
"name": "内部标识",
|
||||||
|
"poolName": "奖池名称",
|
||||||
|
"configNote": "备注",
|
||||||
"poolType": "奖池类型",
|
"poolType": "奖池类型",
|
||||||
"safetyLine": "安全线",
|
"safetyLine": "安全线",
|
||||||
|
"safetyLineNotUsed": "不参与杀分判定",
|
||||||
|
"safetyLineTip": "仅「正常(default)」行有效",
|
||||||
"t1PoolWeight": "T1池权重",
|
"t1PoolWeight": "T1池权重",
|
||||||
"t2PoolWeight": "T2池权重",
|
"t2PoolWeight": "T2池权重",
|
||||||
"t3PoolWeight": "T3池权重",
|
"t3PoolWeight": "T3池权重",
|
||||||
|
|||||||
@@ -57,6 +57,7 @@
|
|||||||
"rollNumber": "摇取点数和",
|
"rollNumber": "摇取点数和",
|
||||||
"rewardTier": "中奖档位",
|
"rewardTier": "中奖档位",
|
||||||
"rewardConfig": "奖励配置",
|
"rewardConfig": "奖励配置",
|
||||||
|
"createTime": "创建时间",
|
||||||
"usernameFuzzy": "用户名模糊",
|
"usernameFuzzy": "用户名模糊",
|
||||||
"nameFuzzy": "名称模糊",
|
"nameFuzzy": "名称模糊",
|
||||||
"uiTextFuzzy": "前端显示文本模糊",
|
"uiTextFuzzy": "前端显示文本模糊",
|
||||||
@@ -84,6 +85,7 @@
|
|||||||
"rollArray": "摇取点数",
|
"rollArray": "摇取点数",
|
||||||
"rollNumber": "摇取点数和",
|
"rollNumber": "摇取点数和",
|
||||||
"rewardTier": "中奖档位",
|
"rewardTier": "中奖档位",
|
||||||
|
"remark": "备注",
|
||||||
"createTime": "创建时间",
|
"createTime": "创建时间",
|
||||||
"updateTime": "更新时间"
|
"updateTime": "更新时间"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,8 @@
|
|||||||
"platformTotalProfit": "平台总盈利"
|
"platformTotalProfit": "平台总盈利"
|
||||||
},
|
},
|
||||||
"search": {
|
"search": {
|
||||||
|
"lotteryPoolConfig": "彩金池配置",
|
||||||
|
"placeholderLotteryPool": "请选择彩金池(可搜索 name)",
|
||||||
"rewardConfigRecordId": "测试记录ID",
|
"rewardConfigRecordId": "测试记录ID",
|
||||||
"drawType": "抽奖类型",
|
"drawType": "抽奖类型",
|
||||||
"direction": "方向",
|
"direction": "方向",
|
||||||
|
|||||||
@@ -22,6 +22,8 @@
|
|||||||
"placeholderLotteryPool": "留空则使用下方自定义权重,或选择彩金池",
|
"placeholderLotteryPool": "留空则使用下方自定义权重,或选择彩金池",
|
||||||
"currentConfig": "当前配置",
|
"currentConfig": "当前配置",
|
||||||
"configLabelName": "名称",
|
"configLabelName": "名称",
|
||||||
|
"configLabelPoolName": "奖池名称",
|
||||||
|
"configLabelCode": "内部标识",
|
||||||
"configLabelType": "类型",
|
"configLabelType": "类型",
|
||||||
"configLabelWeights": "T1~T5 权重",
|
"configLabelWeights": "T1~T5 权重",
|
||||||
"configLabelRemark": "备注",
|
"configLabelRemark": "备注",
|
||||||
@@ -70,7 +72,8 @@
|
|||||||
"placeholderNickname": "请输入昵称",
|
"placeholderNickname": "请输入昵称",
|
||||||
"placeholderPhoneFuzzy": "手机号模糊查询",
|
"placeholderPhoneFuzzy": "手机号模糊查询",
|
||||||
"placeholderAll": "全部",
|
"placeholderAll": "全部",
|
||||||
"exactSearch": "精确搜索"
|
"exactSearch": "精确搜索",
|
||||||
|
"createTime": "注册时间"
|
||||||
},
|
},
|
||||||
"table": {
|
"table": {
|
||||||
"username": "用户名",
|
"username": "用户名",
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
{
|
{
|
||||||
"toolbar": {
|
"toolbar": {
|
||||||
"coinChangeSummary": "平台币变化统计"
|
"coinChangeSummary": "平台币净变化",
|
||||||
|
"coinInflow": "流入",
|
||||||
|
"coinOutflow": "流出"
|
||||||
},
|
},
|
||||||
"form": {
|
"form": {
|
||||||
"dialogTitleAdd": "新增玩家钱包流水",
|
"dialogTitleAdd": "新增玩家钱包流水",
|
||||||
|
|||||||
@@ -25,8 +25,6 @@
|
|||||||
"emptyTier": "该档位暂无配置数据",
|
"emptyTier": "该档位暂无配置数据",
|
||||||
"sumLineDual": "当前档位权重合计(顺时针):{cw};逆时针:{ccw}(各条 1-10000,档位内按权重比抽取,和不限制)",
|
"sumLineDual": "当前档位权重合计(顺时针):{cw};逆时针:{ccw}(各条 1-10000,档位内按权重比抽取,和不限制)",
|
||||||
"sumLineSingle": "当前档位权重合计:{sum}(各条 1-10000,档位内按权重比抽取,和不限制)",
|
"sumLineSingle": "当前档位权重合计:{sum}(各条 1-10000,档位内按权重比抽取,和不限制)",
|
||||||
"t4t5NoteSingle": "T4、T5 仅单一结果,无需配置权重。",
|
|
||||||
"t4t5NoteDual": "T4、T5 档位抽中时仅有一个结果,无需配置权重。",
|
|
||||||
"colEndIndexId": "结束索引(id)",
|
"colEndIndexId": "结束索引(id)",
|
||||||
"colGridNumber": "点数(grid_number)",
|
"colGridNumber": "点数(grid_number)",
|
||||||
"colDicePoints": "色子点数",
|
"colDicePoints": "色子点数",
|
||||||
@@ -45,33 +43,40 @@
|
|||||||
},
|
},
|
||||||
"weightEdit": {
|
"weightEdit": {
|
||||||
"title": "奖励对照表(dice_reward)权重配比",
|
"title": "奖励对照表(dice_reward)权重配比",
|
||||||
"globalTip": "编辑的是奖励对照表(dice_reward / DiceReward 模型)的权重,按结束索引(end_index)区分顺时针与逆时针两套权重;抽奖时按当前方向取对应权重。"
|
"globalTip": "编辑的是奖励对照表(dice_reward / DiceReward 模型)的权重,按结束索引(end_index)区分顺时针与逆时针两套权重;抽奖时按当前方向取对应权重,T4/T5 与 T1-T3 相同支持多点数权重配比。"
|
||||||
},
|
},
|
||||||
"weightRatio": {
|
"weightRatio": {
|
||||||
"title": "权重配比",
|
"title": "权重配比",
|
||||||
"globalTip": "配置奖励对照表(dice_reward)的权重,一级按方向(顺时针/逆时针),二级按档位(T1-T5);各条权重 1-10000,档位内按权重比抽取。",
|
"globalTip": "配置奖励对照表(dice_reward)的权重,一级按方向(顺时针/逆时针),二级按档位(T1-T5);各条权重 1-10000,档位内按权重比抽取骰子点数(T4/T5 与 T1-T3 逻辑相同,可配置多个点数的权重配比)。",
|
||||||
"tabClockwise": "顺时针",
|
"tabClockwise": "顺时针",
|
||||||
"tabCounterclockwise": "逆时针"
|
"tabCounterclockwise": "逆时针"
|
||||||
},
|
},
|
||||||
"weightTest": {
|
"weightTest": {
|
||||||
"title": "一键测试权重",
|
"title": "一键测试权重",
|
||||||
"alertTitle": "彩金池逻辑说明",
|
"alertTitle": "彩金池逻辑说明",
|
||||||
"alertBody": "测试模式默认不启用杀分切换;可通过下方“杀分开关 + 安全线”在测试内启用:当模拟玩家累计盈利达到安全线后,付费抽奖切换到 killScore。",
|
"alertBody": "测试模式默认不启用杀分切换;开启下方「测试内杀分」后,可单独设置测试安全线(与彩金池配置中的安全线无关),用于模拟杀分触发。",
|
||||||
"chainModeHint": "模拟方式:只配置付费抽奖次数(顺/逆时针)。付费抽到「再来一次」或 T5 时,下一局自动为免费抽奖,底注与触发局相同,抽奖类型记为免费、付费金额记为 0。免费抽奖的档位概率由下方「免费抽奖」配置决定(含通过再来一次触发的后续免费局)。",
|
"chainModeHint": "模拟方式:只配置付费抽奖次数(顺/逆时针)。付费抽到「再来一次」或 T5 时,下一局自动为免费抽奖,底注与触发局相同,抽奖类型记为免费、付费金额记为 0。免费抽奖的档位概率由下方「免费抽奖」配置决定(含通过再来一次触发的后续免费局)。",
|
||||||
"killModeHint": "杀分开关开启后:以“模拟玩家累计盈利”作为判定值;当累计盈利 >= 安全线时,后续付费抽奖按 killScore 配置抽取;免费抽奖仍按“免费抽奖配置”执行。",
|
"killModeHint": "杀分开关开启后:从 default 奖池当前 profit_amount 起步逐局累加(付费=win_coin-paid_amount,免费=win_coin);当累计盈利 ≥ 下方「测试安全线」时,后续付费抽奖切 killScore;免费抽奖仍走 name=free 奖池。",
|
||||||
"labelKillModeEnabled": "开启测试内杀分",
|
"killModePanelTitle": "测试内杀分",
|
||||||
|
"killModeSwitchOn": "已开启",
|
||||||
|
"killModeSwitchOff": "已关闭",
|
||||||
"labelTestSafetyLine": "测试安全线",
|
"labelTestSafetyLine": "测试安全线",
|
||||||
|
"testSafetyLineHint": "仅用于本次权重测试,与彩金池配置页的安全线独立;可设较小值以便快速观察杀分切换效果。",
|
||||||
|
"poolProfitRef": "参考:当前 default 池盈利 {profit},彩金池配置安全线 {line}",
|
||||||
|
"killModeOffHint": "关闭时全程按付费/免费配置抽档,不模拟杀分切换。",
|
||||||
"sectionPaid": "付费抽奖",
|
"sectionPaid": "付费抽奖",
|
||||||
"sectionFreeAfterPlayAgain": "免费抽奖(再来一次后的档位概率)",
|
"sectionFreeAfterPlayAgain": "免费抽奖(再来一次)",
|
||||||
"tierProbHintFreeChain": "当使用自定义档位时:以下为「免费抽奖」时 T1~T5 的档位概率(仅在有免费局时参与摇档,与 dice_reward 格子权重共同决定结果)。",
|
"tierProbHintFreeChain": "自定义档位时:免费局 T1~T5 档位概率(与 dice_reward 格子权重共同决定结果)。",
|
||||||
"stepPaid": "付费抽奖券",
|
"stepPaid": "付费抽奖券",
|
||||||
"stepFree": "免费抽奖券",
|
"stepFree": "免费抽奖券",
|
||||||
"labelLotteryTypePaid": "测试数据档位类型",
|
"labelLotteryTypePaid": "付费档位奖池",
|
||||||
"labelLotteryTypeFree": "测试数据档位类型",
|
"labelLotteryTypeFree": "免费档位奖池",
|
||||||
"labelAnte": "底注",
|
"labelAnte": "底注",
|
||||||
"placeholderAnte": "请选择底注配置",
|
"placeholderAnte": "请选择底注配置",
|
||||||
"placeholderPaidPool": "不选则下方自定义档位概率(默认 default)",
|
"anteRandomOption": "随机(每局付费抽奖从当前渠道底注配置中独立抽取)",
|
||||||
"placeholderFreePool": "不选则下方自定义档位概率(默认 killScore)",
|
"placeholderPaidPool": "不选则下方手动设定 T1–T5 档位权重",
|
||||||
|
"placeholderFreePool": "不选则下方手动设定 T1–T5 档位权重",
|
||||||
|
"selectedPoolHint": "已选奖池:{name}",
|
||||||
"tierProbHint": "自定义档位概率(T1~T5),每档 0-100%,五档之和不能超过 100%",
|
"tierProbHint": "自定义档位概率(T1~T5),每档 0-100%,五档之和不能超过 100%",
|
||||||
"tierFieldLabel": "档位 {tier}(%)",
|
"tierFieldLabel": "档位 {tier}(%)",
|
||||||
"tierSumError": "当前五档之和为 {sum}%,不能超过 100%",
|
"tierSumError": "当前五档之和为 {sum}%,不能超过 100%",
|
||||||
|
|||||||
@@ -8,7 +8,19 @@
|
|||||||
"tabIndex": "奖励索引",
|
"tabIndex": "奖励索引",
|
||||||
"tabBigwin": "大奖权重",
|
"tabBigwin": "大奖权重",
|
||||||
"tipIndex": "色子点数须在 5~30 之间且本表内不重复。",
|
"tipIndex": "色子点数须在 5~30 之间且本表内不重复。",
|
||||||
"tipBigwin": "从左至右:中大奖点数(不可改)、显示信息、实际中奖、备注、权重(0~10000)。点数 5、30 权重固定 100%。本表单独立提交,仅提交大奖权重。",
|
"tierRecommendRules": "【结算金额与档位】【大奖】T1:结算金额>2;【小赚】T2:2>=结算金额>1;【抽水】T3:1>=结算金额>0;【惩罚】T4:0>结算金额;【再来一次】T5:0=结算金额。下方可为各档位填写推荐结算金额;表格中「所属档位」随结算金额自动计算,不可手动修改。",
|
||||||
|
"tierRecommendRealEv": "推荐结算金额",
|
||||||
|
"tierRecommendAutoMatch": "修改结算金额时自动匹配档位,并实时更新备注(大奖/小赚/抽水/惩罚/再来一次)",
|
||||||
|
"tierRecommendApplyAmount": "将推荐金额填入已选档位的行",
|
||||||
|
"tierRecommendApplyAmountOk": "已为 {n} 行填入推荐结算金额",
|
||||||
|
"tierRecommendNoTierRows": "没有可根据结算金额推断档位的行",
|
||||||
|
"tierRecommendMatchTier": "按结算金额匹配全部档位",
|
||||||
|
"tierRecommendMatchTierOk": "已根据结算金额为 {n} 行匹配档位",
|
||||||
|
"tierRecommendMatchTierNone": "没有可匹配档位的行",
|
||||||
|
"tierRecommendT5UiText": "再来一次",
|
||||||
|
"tierRecommendT5UiTextEn": "Once again",
|
||||||
|
"colTierAutoHint": "根据结算金额自动匹配",
|
||||||
|
"tipBigwin": "从左至右:中大奖点数(不可改)、显示信息、结算金额、备注、权重(0~10000)。点数 5、30 权重固定 100%。本表单独立提交,仅提交大奖权重。",
|
||||||
"colId": "索引(id)",
|
"colId": "索引(id)",
|
||||||
"colDicePoints": "色子点数",
|
"colDicePoints": "色子点数",
|
||||||
"colDisplayText": "显示文本",
|
"colDisplayText": "显示文本",
|
||||||
@@ -26,7 +38,7 @@
|
|||||||
"colBigwinPoints": "中大奖点数",
|
"colBigwinPoints": "中大奖点数",
|
||||||
"colDisplayInfo": "显示信息",
|
"colDisplayInfo": "显示信息",
|
||||||
"colDisplayInfoEn": "显示信息(英文)",
|
"colDisplayInfoEn": "显示信息(英文)",
|
||||||
"colRealPrize": "实际中奖",
|
"colRealPrize": "结算金额",
|
||||||
"colWeightRange": "权重(0-10000)",
|
"colWeightRange": "权重(0-10000)",
|
||||||
"placeholderDisplayInfoZh": "显示信息(中文)",
|
"placeholderDisplayInfoZh": "显示信息(中文)",
|
||||||
"placeholderDisplayInfoEn": "显示信息(英文)",
|
"placeholderDisplayInfoEn": "显示信息(英文)",
|
||||||
@@ -36,6 +48,19 @@
|
|||||||
"confirmCreateRefMsg": "按规则创建奖励对照:起始索引 start_index=奖励配置中 grid_number 对应格位的 id;顺时针 end_index=(start_index+摇取点数)%26;逆时针 end_index=start_index-摇取点数≥0 则取该值,否则 26+start_index-摇取点数。先清空现有数据再为 5-30 共 26 个点数、顺/逆时针分别生成。是否继续?",
|
"confirmCreateRefMsg": "按规则创建奖励对照:起始索引 start_index=奖励配置中 grid_number 对应格位的 id;顺时针 end_index=(start_index+摇取点数)%26;逆时针 end_index=start_index-摇取点数≥0 则取该值,否则 26+start_index-摇取点数。先清空现有数据再为 5-30 共 26 个点数、顺/逆时针分别生成。是否继续?",
|
||||||
"confirmCreateRefOk": "确定创建",
|
"confirmCreateRefOk": "确定创建",
|
||||||
"confirmCreateRefCancel": "取消",
|
"confirmCreateRefCancel": "取消",
|
||||||
|
"createRefPreviewTitle": "创建奖励对照预览",
|
||||||
|
"createRefPreviewClockwise": "顺时针",
|
||||||
|
"createRefPreviewCounterclockwise": "逆时针",
|
||||||
|
"createRefPreviewTipUnchanged": "检测到色子点数映射未变化:预览中权重将复用当前奖励对照表(dice_reward)的权重;导入时不会覆盖现有权重。",
|
||||||
|
"createRefPreviewTipChanged": "检测到色子点数映射已变化:预览中权重将使用默认值(普通点数默认 100;点数和 10/15/20/25 默认 10;5/30 默认 1);确认导入后可再到「奖励对照」页面调整权重。",
|
||||||
|
"createRefPreviewSkipped": "有 {n} 个点数在当前奖励索引中缺失,已跳过生成(请先补齐 5~30 共 26 个点数)。",
|
||||||
|
"createRefPreviewRefresh": "刷新预览",
|
||||||
|
"createRefPreviewImport": "确认导入",
|
||||||
|
"createRefPreviewImportOk": "已导入奖励对照表",
|
||||||
|
"createRefPreviewImportNoop": "色子点数映射未变化,无需导入(已保留现有权重)",
|
||||||
|
"createRefPreviewDiff": "差异(旧 → 新)",
|
||||||
|
"createRefPreviewNoDiff": "无变化",
|
||||||
|
"createRefPreviewWeightsSaved": "已保存权重",
|
||||||
"createRefSuccess": "已按 5-30 共 26 个点数、顺时针+逆时针创建:顺时针新增 {cwNew} 条、逆时针新增 {ccwNew} 条;顺时针更新 {cwUp} 条、逆时针更新 {ccwUp} 条{skippedPart}",
|
"createRefSuccess": "已按 5-30 共 26 个点数、顺时针+逆时针创建:顺时针新增 {cwNew} 条、逆时针新增 {ccwNew} 条;顺时针更新 {cwUp} 条、逆时针更新 {ccwUp} 条{skippedPart}",
|
||||||
"createRefSuccessSkipped": ";{n} 个点数使用兜底起始索引",
|
"createRefSuccessSkipped": ";{n} 个点数使用兜底起始索引",
|
||||||
"createRefSuccessSimple": "创建成功",
|
"createRefSuccessSimple": "创建成功",
|
||||||
@@ -54,7 +79,7 @@
|
|||||||
"infoNoBigwin": "暂无 BIGWIN 档位配置,请先在「奖励索引」中设置 tier 为 BIGWIN",
|
"infoNoBigwin": "暂无 BIGWIN 档位配置,请先在「奖励索引」中设置 tier 为 BIGWIN",
|
||||||
"btnRuleGenerate": "按规则生成",
|
"btnRuleGenerate": "按规则生成",
|
||||||
"ruleGenerateTitle": "按规则生成奖励索引",
|
"ruleGenerateTitle": "按规则生成奖励索引",
|
||||||
"ruleGenerateRules": "【生成逻辑(与创建奖励对照一致)】\n• 盘面 26 格按 id 升序为位置 0~25;每条配置的 grid_number 为 5~30 且不重复。\n• 摇取点数 D(5~30):起点为「grid_number=D」所在格位的 id(即 start_index),顺时针落点位置 = (起点位置 + D) mod 26,逆时针落点 = 起点位置 − D(若小于 0 则 +26)。\n• 对照表每条记录的「色子点数」列为摇取点数 D;档位、真实结算、显示文案取自落点格位对应 id 的配置。\n\n【豹子摇取点数】\n摇取点数为 5、10、15、20、25、30 时,其顺/逆时针落点档位不能为 T4、T5(避免对照表上出现豹子点数 + 惩罚/再来一次)。\n\n【结算金额 与 档位】\n结算金额 < 0 → T4;0 < 结算金额 < 100 → T3;100 < 结算金额 < 200 → T2;200 < 结算金额 → T1;T5「再来一次」结算金额=0。下方可为各档位填写统一的 结算金额 标准,生成时写入配置;细则可稍后在表格中再改。\n\n【本弹窗输入】\n条数:T1/T4/T5「固定」;T2「不少于」——顺时针与逆时针的加权条数(每条摇取结果计一次)须分别满足所填数值;T1、T4 与 T5 分开填写。\n结算金额 标准:同档位各格使用同一数值。生成时 T1~T4 的 显示文本 / 显示文本(英文) = 结算金额;T5 固定为「再来一次」/「Once again」。备注仍区分完美回本/小赚等。",
|
"ruleGenerateRules": "【生成逻辑(与创建奖励对照一致)】\n• 盘面 26 格按 id 升序为位置 0~25;每条配置的 grid_number 为 5~30 且不重复。\n• 摇取点数 D(5~30):起点为「grid_number=D」所在格位的 id(即 start_index),顺时针落点位置 = (起点位置 + D) mod 26,逆时针落点 = 起点位置 − D(若小于 0 则 +26)。\n• 对照表每条记录的「色子点数」列为摇取点数 D;档位、真实结算、显示文案取自落点格位对应 id 的配置。\n\n【豹子摇取点数】\n摇取点数为 5、10、15、20、25、30 时,其顺/逆时针落点档位不能为 T4、T5(避免对照表上出现豹子点数 + 惩罚/再来一次)。\n\n【结算金额 与 档位】\n【大奖】T1:>2;【小赚】T2:2>=金额>1;【抽水】T3:1>=金额>0;【惩罚】T4:0>金额;【再来一次】T5:0=金额。下方可为各档位填写推荐结算金额标准,生成时写入配置。\n\n【本弹窗输入】\n条数:T1/T4/T5「固定」;T2「不少于」——顺时针与逆时针的加权条数(每条摇取结果计一次)须分别满足所填数值;T1、T4 与 T5 分开填写。\n结算金额 标准:同档位各格使用同一数值。生成时 T1~T4 的显示文本 = 结算金额;T5 固定为「再来一次」/「Once again」。备注仍区分完美回本/小赚等。",
|
||||||
"ruleGenT1Row": "T1 大奖",
|
"ruleGenT1Row": "T1 大奖",
|
||||||
"ruleGenT2Row": "T2 小赚/回本",
|
"ruleGenT2Row": "T2 小赚/回本",
|
||||||
"ruleGenT3RealEvOnly": "T3 抽水",
|
"ruleGenT3RealEvOnly": "T3 抽水",
|
||||||
@@ -64,11 +89,11 @@
|
|||||||
"ruleGenFixedCount": "固定条数(顺/逆)",
|
"ruleGenFixedCount": "固定条数(顺/逆)",
|
||||||
"ruleGenRealEvStd": "结算金额",
|
"ruleGenRealEvStd": "结算金额",
|
||||||
"ruleGenRealEvEditHint": "生成并保存后,仍可在本页表格中逐条修改显示文案、英文、真实结算与备注。",
|
"ruleGenRealEvEditHint": "生成并保存后,仍可在本页表格中逐条修改显示文案、英文、真实结算与备注。",
|
||||||
"ruleGenInvalidT1RealEv": "T1 的 结算金额 满足:200 < 值",
|
"ruleGenInvalidT1RealEv": "T1(大奖)结算金额须满足:值 > 2",
|
||||||
"ruleGenInvalidT2RealEv": "T2 的 结算金额 满足:100 < 值 < 200",
|
"ruleGenInvalidT2RealEv": "T2(小赚)结算金额须满足:1 < 值 ≤ 2",
|
||||||
"ruleGenInvalidT3RealEv": "T3 的 结算金额 满足:0 < 值 < 100",
|
"ruleGenInvalidT3RealEv": "T3(抽水)结算金额须满足:0 < 值 ≤ 1",
|
||||||
"ruleGenInvalidT4RealEv": "T4 的 结算金额 满足:值 < 0",
|
"ruleGenInvalidT4RealEv": "T4(惩罚)结算金额须满足:值 < 0",
|
||||||
"ruleGenInvalidT5RealEv": "T5「再来一次」的 结算金额 须为 0",
|
"ruleGenInvalidT5RealEv": "T5(再来一次)结算金额须为 0",
|
||||||
"ruleGenT1Min": "T1 固定条数(顺/逆)",
|
"ruleGenT1Min": "T1 固定条数(顺/逆)",
|
||||||
"ruleGenT2Min": "T2 最少条数(顺/逆)",
|
"ruleGenT2Min": "T2 最少条数(顺/逆)",
|
||||||
"ruleGenT4Max": "T4 固定条数(顺/逆)",
|
"ruleGenT4Max": "T4 固定条数(顺/逆)",
|
||||||
|
|||||||
@@ -17,6 +17,8 @@
|
|||||||
"chainModeNo": "否",
|
"chainModeNo": "否",
|
||||||
"paidPlannedSpins": "计划付费次数",
|
"paidPlannedSpins": "计划付费次数",
|
||||||
"ante": "底注",
|
"ante": "底注",
|
||||||
|
"anteRandom": "随机",
|
||||||
|
"testSafetyLine": "安全线",
|
||||||
"playAgainCount": "再来一次次数",
|
"playAgainCount": "再来一次次数",
|
||||||
"progressDraws": "已完成 {over} 次",
|
"progressDraws": "已完成 {over} 次",
|
||||||
"progressFailed": "失败前 {over} 次",
|
"progressFailed": "失败前 {over} 次",
|
||||||
@@ -51,11 +53,13 @@
|
|||||||
"testCountProgress": "进行中:已完成 {over} 次",
|
"testCountProgress": "进行中:已完成 {over} 次",
|
||||||
"testCountFailed": "失败前 {over} 次",
|
"testCountFailed": "失败前 {over} 次",
|
||||||
"chainModeLabel": "链式再来一次",
|
"chainModeLabel": "链式再来一次",
|
||||||
|
"killModeOff": "未开启杀分",
|
||||||
"paidPlannedSpins": "计划付费次数",
|
"paidPlannedSpins": "计划付费次数",
|
||||||
|
"testSafetyLine": "测试安全线",
|
||||||
"createTime": "创建时间",
|
"createTime": "创建时间",
|
||||||
"admin": "执行管理员",
|
"admin": "执行管理员",
|
||||||
"paidPoolId": "付费奖池配置ID",
|
"paidPoolId": "付费彩金池",
|
||||||
"freePoolId": "免费奖池配置ID",
|
"freePoolId": "免费彩金池",
|
||||||
"bigwinSnapshot": "BIGWIN 权重快照",
|
"bigwinSnapshot": "BIGWIN 权重快照",
|
||||||
"sectionPaidTier": "付费抽奖档位概率(T1-T5,测试时使用)",
|
"sectionPaidTier": "付费抽奖档位概率(T1-T5,测试时使用)",
|
||||||
"sectionFreeTier": "免费抽奖档位概率(T1-T5,测试时使用)",
|
"sectionFreeTier": "免费抽奖档位概率(T1-T5,测试时使用)",
|
||||||
|
|||||||
27
saiadmin-artd/src/locales/langs/zh/system/admin_guide.json
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"title": "后台操作指南",
|
||||||
|
"toolbar": {
|
||||||
|
"edit": "编辑",
|
||||||
|
"save": "保存",
|
||||||
|
"cancel": "取消",
|
||||||
|
"refresh": "刷新"
|
||||||
|
},
|
||||||
|
"meta": {
|
||||||
|
"filePath": "文档路径",
|
||||||
|
"updateTime": "更新时间"
|
||||||
|
},
|
||||||
|
"catalog": {
|
||||||
|
"title": "目录",
|
||||||
|
"empty": "暂无目录"
|
||||||
|
},
|
||||||
|
"image": {
|
||||||
|
"zoom": "点击查看原图"
|
||||||
|
},
|
||||||
|
"message": {
|
||||||
|
"loadFailed": "加载操作指南失败",
|
||||||
|
"saveSuccess": "保存成功",
|
||||||
|
"saveFailed": "保存失败",
|
||||||
|
"cancelConfirm": "当前有未保存的修改,确定取消编辑吗?",
|
||||||
|
"editRequired": "请先点击编辑后再保存"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -46,12 +46,21 @@ export async function loadPageLocale(routePath: string): Promise<void> {
|
|||||||
const modules = locale === LanguageEnum.EN ? enModules : zhModules
|
const modules = locale === LanguageEnum.EN ? enModules : zhModules
|
||||||
|
|
||||||
const tryPaths: string[] = [path]
|
const tryPaths: string[] = [path]
|
||||||
// 兼容别名路由:例如 /user 实际页面为 /system/user
|
// 兼容别名路由:菜单 path 为短名但 locale 文件位于模块子目录
|
||||||
|
// 例如:/user -> system/user,/game -> dice/game
|
||||||
if (!path.includes('/')) {
|
if (!path.includes('/')) {
|
||||||
tryPaths.push(`system/${path}`)
|
tryPaths.push(`system/${path}`)
|
||||||
|
// 兜底:在任意一级子目录下查找同名文件
|
||||||
|
const suffix = `/${path}.json`
|
||||||
|
const localePrefix = `./langs/${locale}/`
|
||||||
|
for (const key of Object.keys(modules)) {
|
||||||
|
if (key.startsWith(localePrefix) && key.endsWith(suffix)) {
|
||||||
|
const candidate = key.slice(localePrefix.length, -'.json'.length)
|
||||||
|
if (!tryPaths.includes(candidate)) {
|
||||||
|
tryPaths.push(candidate)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (path === 'user') {
|
|
||||||
tryPaths.push('system/user')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let matchedPath: string | null = null
|
let matchedPath: string | null = null
|
||||||
|
|||||||
@@ -7,6 +7,23 @@ const BUILTIN_CHANNEL_LAYOUT_PATHS = [
|
|||||||
'/system/dept'
|
'/system/dept'
|
||||||
]
|
]
|
||||||
|
|
||||||
|
/** 运维页里不需要按渠道分栏的页面 */
|
||||||
|
const NO_CHANNEL_LAYOUT_PATHS = [
|
||||||
|
'/safeguard/dict',
|
||||||
|
'/safeguard/attachment',
|
||||||
|
'/safeguard/database',
|
||||||
|
'/safeguard/server',
|
||||||
|
'/safeguard/cache',
|
||||||
|
'/safeguard/email-log',
|
||||||
|
'/admin_guide'
|
||||||
|
]
|
||||||
|
|
||||||
|
/** 日志页:左侧首项为「全部」,dept_id=0 表示不按渠道过滤 */
|
||||||
|
const ALL_CHANNEL_SCOPE_PATHS = [
|
||||||
|
'/safeguard/login-log',
|
||||||
|
'/safeguard/oper-log'
|
||||||
|
]
|
||||||
|
|
||||||
export function isSuperAdminUser(): boolean {
|
export function isSuperAdminUser(): boolean {
|
||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
return Number(userStore.info?.id ?? 0) === 1
|
return Number(userStore.info?.id ?? 0) === 1
|
||||||
@@ -28,6 +45,20 @@ export function isRoleChannelRoute(route: Pick<RouteLocationNormalized, 'path' |
|
|||||||
return route.path.startsWith('/system/role')
|
return route.path.startsWith('/system/role')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isAllChannelScopeRoute(route: Pick<RouteLocationNormalized, 'path' | 'meta'>): boolean {
|
||||||
|
if (route.meta?.channelScope === 'all') {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return ALL_CHANNEL_SCOPE_PATHS.some((item) => route.path.startsWith(item))
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isNoChannelLayoutRoute(route: Pick<RouteLocationNormalized, 'path' | 'meta'>): boolean {
|
||||||
|
if (route.meta?.noChannelLayout === true) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return NO_CHANNEL_LAYOUT_PATHS.some((item) => route.path.startsWith(item))
|
||||||
|
}
|
||||||
|
|
||||||
export function shouldWrapSuperAdminChannelLayout(route: RouteLocationNormalized): boolean {
|
export function shouldWrapSuperAdminChannelLayout(route: RouteLocationNormalized): boolean {
|
||||||
if (!isSuperAdminUser()) {
|
if (!isSuperAdminUser()) {
|
||||||
return false
|
return false
|
||||||
@@ -35,10 +66,10 @@ export function shouldWrapSuperAdminChannelLayout(route: RouteLocationNormalized
|
|||||||
if (route.meta?.isFullPage) {
|
if (route.meta?.isFullPage) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
if (route.meta?.noChannelLayout === true) {
|
const path = route.path
|
||||||
|
if (isNoChannelLayoutRoute(route)) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
const path = route.path
|
|
||||||
for (let i = 0; i < BUILTIN_CHANNEL_LAYOUT_PATHS.length; i++) {
|
for (let i = 0; i < BUILTIN_CHANNEL_LAYOUT_PATHS.length; i++) {
|
||||||
if (path.startsWith(BUILTIN_CHANNEL_LAYOUT_PATHS[i])) {
|
if (path.startsWith(BUILTIN_CHANNEL_LAYOUT_PATHS[i])) {
|
||||||
return false
|
return false
|
||||||
|
|||||||
@@ -16,10 +16,11 @@
|
|||||||
*/
|
*/
|
||||||
import { AppRouteRecord } from '@/types/router'
|
import { AppRouteRecord } from '@/types/router'
|
||||||
import { router } from '@/router'
|
import { router } from '@/router'
|
||||||
|
import { resolveAppAssetUrl } from '@/utils/navigation/resolveAppAssetUrl'
|
||||||
|
|
||||||
// 打开外部链接
|
// 打开外部链接(含站内静态页,自动拼接部署 base)
|
||||||
export const openExternalLink = (link: string) => {
|
export const openExternalLink = (link: string) => {
|
||||||
window.open(link, '_blank')
|
window.open(resolveAppAssetUrl(link), '_blank', 'noopener,noreferrer')
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
19
saiadmin-artd/src/utils/navigation/resolveAppAssetUrl.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
/**
|
||||||
|
* 解析菜单外链 / 静态资源路径(兼容 VITE_BASE_URL 子目录部署)
|
||||||
|
*/
|
||||||
|
export function resolveAppAssetUrl(relativeOrAbsolute: string): string {
|
||||||
|
const link = relativeOrAbsolute.trim()
|
||||||
|
if (!link) {
|
||||||
|
return link
|
||||||
|
}
|
||||||
|
if (/^https?:\/\//i.test(link)) {
|
||||||
|
return link
|
||||||
|
}
|
||||||
|
const base = import.meta.env.BASE_URL || '/'
|
||||||
|
const normalizedBase = base.endsWith('/') ? base : `${base}/`
|
||||||
|
const path = link.startsWith('/') ? link.slice(1) : link
|
||||||
|
if (typeof window === 'undefined') {
|
||||||
|
return `${normalizedBase}${path}`
|
||||||
|
}
|
||||||
|
return `${window.location.origin}${normalizedBase}${path}`
|
||||||
|
}
|
||||||
@@ -61,6 +61,10 @@ export const MAP_PATH_TO_MENU_I18N_KEY: Record<string, string> = {
|
|||||||
'/dice/play_record_test/index': 'menus.dice.playRecordTest',
|
'/dice/play_record_test/index': 'menus.dice.playRecordTest',
|
||||||
'/dice/config': 'menus.dice.config',
|
'/dice/config': 'menus.dice.config',
|
||||||
'/dice/config/index': 'menus.dice.config',
|
'/dice/config/index': 'menus.dice.config',
|
||||||
|
'game': 'menus.game.title',
|
||||||
|
'game/index': 'menus.game.title',
|
||||||
|
'/game': 'menus.game.title',
|
||||||
|
'/game/index': 'menus.game.title',
|
||||||
'/result/success': 'menus.result.success',
|
'/result/success': 'menus.result.success',
|
||||||
'/result/fail': 'menus.result.fail',
|
'/result/fail': 'menus.result.fail',
|
||||||
'/exception/403': 'menus.exception.forbidden',
|
'/exception/403': 'menus.exception.forbidden',
|
||||||
|
|||||||
@@ -1,6 +1,22 @@
|
|||||||
<!-- 工作台页面:大富翁色子游戏数据统计 -->
|
<!-- 工作台页面:大富翁色子游戏数据统计 -->
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
|
<div class="dashboard-filter-bar art-card flex flex-wrap items-center gap-3 px-5 py-3 mb-5 max-sm:mb-4">
|
||||||
|
<span class="text-g-700 text-sm shrink-0">{{ $t('console.filter.date') }}</span>
|
||||||
|
<ElDatePicker
|
||||||
|
v-model="selectedDate"
|
||||||
|
type="date"
|
||||||
|
value-format="YYYY-MM-DD"
|
||||||
|
clearable
|
||||||
|
:placeholder="$t('console.filter.placeholder')"
|
||||||
|
class="dashboard-date-picker"
|
||||||
|
/>
|
||||||
|
<ElButton type="primary" link @click="resetToToday">{{ $t('console.filter.today') }}</ElButton>
|
||||||
|
<ElButton v-if="selectedDate" type="primary" link @click="clearDateFilter">
|
||||||
|
{{ $t('console.filter.clear') }}
|
||||||
|
</ElButton>
|
||||||
|
</div>
|
||||||
|
|
||||||
<CardList />
|
<CardList />
|
||||||
|
|
||||||
<template v-if="isStatisticsDashboard">
|
<template v-if="isStatisticsDashboard">
|
||||||
@@ -40,10 +56,20 @@
|
|||||||
import PlayRecordList from './modules/play-record-list.vue'
|
import PlayRecordList from './modules/play-record-list.vue'
|
||||||
import { useCommon } from '@/hooks/core/useCommon'
|
import { useCommon } from '@/hooks/core/useCommon'
|
||||||
import { useUserStore } from '@/store/modules/user'
|
import { useUserStore } from '@/store/modules/user'
|
||||||
|
import { getTodayDateString, provideDashboardScope } from '@/composables/useDashboardScope'
|
||||||
|
|
||||||
defineOptions({ name: 'Console' })
|
defineOptions({ name: 'Console' })
|
||||||
|
|
||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
|
const { selectedDate } = provideDashboardScope()
|
||||||
|
|
||||||
|
const resetToToday = () => {
|
||||||
|
selectedDate.value = getTodayDateString()
|
||||||
|
}
|
||||||
|
|
||||||
|
const clearDateFilter = () => {
|
||||||
|
selectedDate.value = null
|
||||||
|
}
|
||||||
|
|
||||||
/** 统计页额外展示充值图表 */
|
/** 统计页额外展示充值图表 */
|
||||||
const isStatisticsDashboard = computed(
|
const isStatisticsDashboard = computed(
|
||||||
@@ -53,3 +79,14 @@
|
|||||||
const { scrollToTop } = useCommon()
|
const { scrollToTop } = useCommon()
|
||||||
scrollToTop()
|
scrollToTop()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.dashboard-filter-bar {
|
||||||
|
min-height: 52px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-date-picker {
|
||||||
|
width: 180px;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
<template>
|
<template>
|
||||||
<ElRow :gutter="20" class="flex">
|
<ElRow :gutter="20" class="flex">
|
||||||
<ElCol :sm="12" :md="6" :lg="6">
|
<ElCol :sm="12" :md="6" :lg="6">
|
||||||
<div class="art-card relative flex flex-col justify-center h-35 px-5 mb-5 max-sm:mb-4">
|
<div
|
||||||
|
class="art-card relative flex flex-col justify-center h-35 px-5 mb-5 max-sm:mb-4 dashboard-stat-card dashboard-stat-card--clickable"
|
||||||
|
@click="goPlayerList"
|
||||||
|
>
|
||||||
<span class="text-g-700 text-sm">{{ $t('console.card.playerRegister') }}</span>
|
<span class="text-g-700 text-sm">{{ $t('console.card.playerRegister') }}</span>
|
||||||
<ArtCountTo class="text-[26px] font-medium mt-2" :target="statData.player_count" :duration="1300" />
|
<ArtCountTo class="text-[26px] font-medium mt-2" :target="statData.player_count" :duration="1300" />
|
||||||
<div class="flex-c mt-1">
|
<div class="flex-c mt-1">
|
||||||
<span class="text-xs text-g-600">{{ $t('console.card.vsLastWeek') }}</span>
|
<span class="text-xs text-g-600">{{ compareLabel }}</span>
|
||||||
<span
|
<span
|
||||||
class="ml-1 text-xs font-semibold"
|
class="ml-1 text-xs font-semibold"
|
||||||
:class="changeClass(statData.player_count_change)"
|
:class="changeClass(statData.player_count_change)"
|
||||||
@@ -13,6 +16,9 @@
|
|||||||
{{ formatChange(statData.player_count_change) }}
|
{{ formatChange(statData.player_count_change) }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
<ElButton type="primary" link class="dashboard-stat-link" @click.stop="goPlayerList">
|
||||||
|
{{ $t('console.card.viewRegisterRecords') }}
|
||||||
|
</ElButton>
|
||||||
<div
|
<div
|
||||||
class="absolute top-0 bottom-0 right-5 m-auto size-12.5 rounded-xl flex-cc bg-theme/10"
|
class="absolute top-0 bottom-0 right-5 m-auto size-12.5 rounded-xl flex-cc bg-theme/10"
|
||||||
>
|
>
|
||||||
@@ -21,7 +27,10 @@
|
|||||||
</div>
|
</div>
|
||||||
</ElCol>
|
</ElCol>
|
||||||
<ElCol :sm="12" :md="6" :lg="6">
|
<ElCol :sm="12" :md="6" :lg="6">
|
||||||
<div class="art-card relative flex flex-col justify-center h-35 px-5 mb-5 max-sm:mb-4">
|
<div
|
||||||
|
class="art-card relative flex flex-col justify-center h-35 px-5 mb-5 max-sm:mb-4 dashboard-stat-card dashboard-stat-card--clickable"
|
||||||
|
@click="goWalletRecord(0)"
|
||||||
|
>
|
||||||
<span class="text-g-700 text-sm">{{ $t('console.card.playerCharge') }}</span>
|
<span class="text-g-700 text-sm">{{ $t('console.card.playerCharge') }}</span>
|
||||||
<ArtCountTo
|
<ArtCountTo
|
||||||
class="text-[26px] font-medium mt-2"
|
class="text-[26px] font-medium mt-2"
|
||||||
@@ -30,7 +39,7 @@
|
|||||||
:decimals="2"
|
:decimals="2"
|
||||||
/>
|
/>
|
||||||
<div class="flex-c mt-1">
|
<div class="flex-c mt-1">
|
||||||
<span class="text-xs text-g-600">{{ $t('console.card.vsLastWeek') }}</span>
|
<span class="text-xs text-g-600">{{ compareLabel }}</span>
|
||||||
<span
|
<span
|
||||||
class="ml-1 text-xs font-semibold"
|
class="ml-1 text-xs font-semibold"
|
||||||
:class="changeClass(statData.charge_amount_change)"
|
:class="changeClass(statData.charge_amount_change)"
|
||||||
@@ -38,6 +47,9 @@
|
|||||||
{{ formatChange(statData.charge_amount_change) }}
|
{{ formatChange(statData.charge_amount_change) }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
<ElButton type="primary" link class="dashboard-stat-link" @click.stop="goWalletRecord(0)">
|
||||||
|
{{ $t('console.card.viewRechargeRecords') }}
|
||||||
|
</ElButton>
|
||||||
<div
|
<div
|
||||||
class="absolute top-0 bottom-0 right-5 m-auto size-12.5 rounded-xl flex-cc bg-theme/10"
|
class="absolute top-0 bottom-0 right-5 m-auto size-12.5 rounded-xl flex-cc bg-theme/10"
|
||||||
>
|
>
|
||||||
@@ -46,7 +58,10 @@
|
|||||||
</div>
|
</div>
|
||||||
</ElCol>
|
</ElCol>
|
||||||
<ElCol :sm="12" :md="6" :lg="6">
|
<ElCol :sm="12" :md="6" :lg="6">
|
||||||
<div class="art-card relative flex flex-col justify-center h-35 px-5 mb-5 max-sm:mb-4">
|
<div
|
||||||
|
class="art-card relative flex flex-col justify-center h-35 px-5 mb-5 max-sm:mb-4 dashboard-stat-card dashboard-stat-card--clickable"
|
||||||
|
@click="goWalletRecord(1)"
|
||||||
|
>
|
||||||
<span class="text-g-700 text-sm">{{ $t('console.card.playerWithdraw') }}</span>
|
<span class="text-g-700 text-sm">{{ $t('console.card.playerWithdraw') }}</span>
|
||||||
<ArtCountTo
|
<ArtCountTo
|
||||||
class="text-[26px] font-medium mt-2"
|
class="text-[26px] font-medium mt-2"
|
||||||
@@ -55,7 +70,7 @@
|
|||||||
:decimals="2"
|
:decimals="2"
|
||||||
/>
|
/>
|
||||||
<div class="flex-c mt-1">
|
<div class="flex-c mt-1">
|
||||||
<span class="text-xs text-g-600">{{ $t('console.card.vsLastWeek') }}</span>
|
<span class="text-xs text-g-600">{{ compareLabel }}</span>
|
||||||
<span
|
<span
|
||||||
class="ml-1 text-xs font-semibold"
|
class="ml-1 text-xs font-semibold"
|
||||||
:class="changeClass(statData.withdraw_amount_change)"
|
:class="changeClass(statData.withdraw_amount_change)"
|
||||||
@@ -63,6 +78,9 @@
|
|||||||
{{ formatChange(statData.withdraw_amount_change) }}
|
{{ formatChange(statData.withdraw_amount_change) }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
<ElButton type="primary" link class="dashboard-stat-link" @click.stop="goWalletRecord(1)">
|
||||||
|
{{ $t('console.card.viewWithdrawRecords') }}
|
||||||
|
</ElButton>
|
||||||
<div
|
<div
|
||||||
class="absolute top-0 bottom-0 right-5 m-auto size-12.5 rounded-xl flex-cc bg-theme/10"
|
class="absolute top-0 bottom-0 right-5 m-auto size-12.5 rounded-xl flex-cc bg-theme/10"
|
||||||
>
|
>
|
||||||
@@ -71,7 +89,10 @@
|
|||||||
</div>
|
</div>
|
||||||
</ElCol>
|
</ElCol>
|
||||||
<ElCol :sm="12" :md="6" :lg="6">
|
<ElCol :sm="12" :md="6" :lg="6">
|
||||||
<div class="art-card relative flex flex-col justify-center h-35 px-5 mb-5 max-sm:mb-4">
|
<div
|
||||||
|
class="art-card relative flex flex-col justify-center h-35 px-5 mb-5 max-sm:mb-4 dashboard-stat-card dashboard-stat-card--clickable"
|
||||||
|
@click="goPlayRecord"
|
||||||
|
>
|
||||||
<span class="text-g-700 text-sm">{{ $t('console.card.playerPlayCount') }}</span>
|
<span class="text-g-700 text-sm">{{ $t('console.card.playerPlayCount') }}</span>
|
||||||
<ArtCountTo
|
<ArtCountTo
|
||||||
class="text-[26px] font-medium mt-2"
|
class="text-[26px] font-medium mt-2"
|
||||||
@@ -79,7 +100,7 @@
|
|||||||
:duration="1300"
|
:duration="1300"
|
||||||
/>
|
/>
|
||||||
<div class="flex-c mt-1">
|
<div class="flex-c mt-1">
|
||||||
<span class="text-xs text-g-600">{{ $t('console.card.vsLastWeek') }}</span>
|
<span class="text-xs text-g-600">{{ compareLabel }}</span>
|
||||||
<span
|
<span
|
||||||
class="ml-1 text-xs font-semibold"
|
class="ml-1 text-xs font-semibold"
|
||||||
:class="changeClass(statData.play_count_change)"
|
:class="changeClass(statData.play_count_change)"
|
||||||
@@ -87,6 +108,9 @@
|
|||||||
{{ formatChange(statData.play_count_change) }}
|
{{ formatChange(statData.play_count_change) }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
<ElButton type="primary" link class="dashboard-stat-link" @click.stop="goPlayRecord">
|
||||||
|
{{ $t('console.card.viewPlayRecords') }}
|
||||||
|
</ElButton>
|
||||||
<div
|
<div
|
||||||
class="absolute top-0 bottom-0 right-5 m-auto size-12.5 rounded-xl flex-cc bg-theme/10"
|
class="absolute top-0 bottom-0 right-5 m-auto size-12.5 rounded-xl flex-cc bg-theme/10"
|
||||||
>
|
>
|
||||||
@@ -99,7 +123,21 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { fetchStatistics } from '@/api/dashboard'
|
import { fetchStatistics } from '@/api/dashboard'
|
||||||
import { getChannelDeptRequestParams, useChannelDeptReload } from '@/composables/useChannelDeptScope'
|
import { useDashboardReload, useDashboardScope } from '@/composables/useDashboardScope'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import {
|
||||||
|
dashboardDateNavParams,
|
||||||
|
openPlayRecord,
|
||||||
|
openPlayerList,
|
||||||
|
openWalletRecord
|
||||||
|
} from '@/views/plugin/dice/utils/dashboardRecordNav'
|
||||||
|
|
||||||
|
const { t } = useI18n()
|
||||||
|
const { queryParams, hasDateFilter, selectedDate } = useDashboardScope()
|
||||||
|
|
||||||
|
const compareLabel = computed(() =>
|
||||||
|
hasDateFilter.value ? t('console.card.vsYesterday') : t('console.card.vsLastWeek')
|
||||||
|
)
|
||||||
|
|
||||||
const statData = ref({
|
const statData = ref({
|
||||||
player_count: 0,
|
player_count: 0,
|
||||||
@@ -125,7 +163,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
const loadStatistics = () => {
|
const loadStatistics = () => {
|
||||||
fetchStatistics(getChannelDeptRequestParams()).then((data: any) => {
|
fetchStatistics(queryParams.value).then((data: any) => {
|
||||||
statData.value = {
|
statData.value = {
|
||||||
player_count: data?.player_count ?? 0,
|
player_count: data?.player_count ?? 0,
|
||||||
player_count_change: data?.player_count_change ?? 0,
|
player_count_change: data?.player_count_change ?? 0,
|
||||||
@@ -139,5 +177,39 @@
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
useChannelDeptReload(loadStatistics)
|
useDashboardReload(loadStatistics)
|
||||||
|
|
||||||
|
const goWalletRecord = (type: number) => {
|
||||||
|
openWalletRecord({
|
||||||
|
type,
|
||||||
|
...dashboardDateNavParams(selectedDate.value)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const goPlayRecord = () => {
|
||||||
|
openPlayRecord(dashboardDateNavParams(selectedDate.value))
|
||||||
|
}
|
||||||
|
|
||||||
|
const goPlayerList = () => {
|
||||||
|
openPlayerList(dashboardDateNavParams(selectedDate.value))
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.dashboard-stat-card--clickable {
|
||||||
|
cursor: pointer;
|
||||||
|
transition: box-shadow 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-stat-card--clickable:hover {
|
||||||
|
box-shadow: 0 4px 12px rgb(0 0 0 / 8%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-stat-link {
|
||||||
|
align-self: flex-start;
|
||||||
|
margin-top: 4px;
|
||||||
|
padding: 0;
|
||||||
|
height: auto;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="art-card p-5 overflow-hidden mb-5 max-sm:mb-4">
|
<div class="art-card p-5 overflow-hidden mb-5 max-sm:mb-4">
|
||||||
<div class="art-card-header mb-4">
|
<div class="art-card-header mb-4 flex items-start justify-between gap-3">
|
||||||
<div class="title">
|
<div class="title">
|
||||||
<h4>{{ $t('console.newPlayer.title') }}</h4>
|
<h4>{{ $t('console.newPlayer.title') }}</h4>
|
||||||
<p class="text-g-600 text-sm mt-1">{{ $t('console.newPlayer.subtitle') }}</p>
|
<p class="text-g-600 text-sm mt-1">{{ listSubtitle }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
<ElButton type="primary" link @click="goPlayerList">
|
||||||
|
{{ $t('console.nav.viewAllRegister') }}
|
||||||
|
</ElButton>
|
||||||
</div>
|
</div>
|
||||||
<ArtTable
|
<ArtTable
|
||||||
class="w-full"
|
class="w-full"
|
||||||
@@ -41,20 +44,37 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { fetchNewPlayerList, type NewPlayerItem } from '@/api/dashboard'
|
import { fetchNewPlayerList, type NewPlayerItem } from '@/api/dashboard'
|
||||||
import { getChannelDeptRequestParams, useChannelDeptReload } from '@/composables/useChannelDeptScope'
|
import { useDashboardReload, useDashboardScope } from '@/composables/useDashboardScope'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import {
|
||||||
|
dashboardDateNavParams,
|
||||||
|
openPlayerList
|
||||||
|
} from '@/views/plugin/dice/utils/dashboardRecordNav'
|
||||||
|
|
||||||
|
const { t } = useI18n()
|
||||||
|
const { queryParams, selectedDate, hasDateFilter } = useDashboardScope()
|
||||||
const tableData = ref<NewPlayerItem[]>([])
|
const tableData = ref<NewPlayerItem[]>([])
|
||||||
|
|
||||||
|
const listSubtitle = computed(() =>
|
||||||
|
hasDateFilter.value && selectedDate.value
|
||||||
|
? t('console.newPlayer.subtitleByDate', { date: selectedDate.value })
|
||||||
|
: t('console.newPlayer.subtitle')
|
||||||
|
)
|
||||||
|
|
||||||
function formatCoin(val: number | undefined): string {
|
function formatCoin(val: number | undefined): string {
|
||||||
if (val === undefined || val === null) return '0.00'
|
if (val === undefined || val === null) return '0.00'
|
||||||
return Number(val).toFixed(2)
|
return Number(val).toFixed(2)
|
||||||
}
|
}
|
||||||
|
|
||||||
const loadList = () => {
|
const loadList = () => {
|
||||||
fetchNewPlayerList(getChannelDeptRequestParams()).then((data) => {
|
fetchNewPlayerList(queryParams.value).then((data) => {
|
||||||
tableData.value = Array.isArray(data) ? data : []
|
tableData.value = Array.isArray(data) ? data : []
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
useChannelDeptReload(loadList)
|
useDashboardReload(loadList)
|
||||||
|
|
||||||
|
const goPlayerList = () => {
|
||||||
|
openPlayerList(dashboardDateNavParams(selectedDate.value))
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="art-card p-5 overflow-hidden mb-5 max-sm:mb-4">
|
<div class="art-card p-5 overflow-hidden mb-5 max-sm:mb-4">
|
||||||
<div class="art-card-header mb-4">
|
<div class="art-card-header mb-4 flex items-start justify-between gap-3">
|
||||||
<div class="title">
|
<div class="title">
|
||||||
<h4>{{ $t('console.playRecord.title') }}</h4>
|
<h4>{{ $t('console.playRecord.title') }}</h4>
|
||||||
<p class="text-g-600 text-sm mt-1">{{ $t('console.playRecord.subtitle') }}</p>
|
<p class="text-g-600 text-sm mt-1">{{ listSubtitle }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
<ElButton type="primary" link @click="goPlayRecords">
|
||||||
|
{{ $t('console.nav.viewAllPlay') }}
|
||||||
|
</ElButton>
|
||||||
</div>
|
</div>
|
||||||
<ArtTable
|
<ArtTable
|
||||||
class="w-full"
|
class="w-full"
|
||||||
@@ -51,20 +54,37 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { fetchPlayRecordList, type PlayRecordItem } from '@/api/dashboard'
|
import { fetchPlayRecordList, type PlayRecordItem } from '@/api/dashboard'
|
||||||
import { getChannelDeptRequestParams, useChannelDeptReload } from '@/composables/useChannelDeptScope'
|
import { useDashboardReload, useDashboardScope } from '@/composables/useDashboardScope'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import {
|
||||||
|
dashboardDateNavParams,
|
||||||
|
openPlayRecord
|
||||||
|
} from '@/views/plugin/dice/utils/dashboardRecordNav'
|
||||||
|
|
||||||
|
const { t } = useI18n()
|
||||||
|
const { queryParams, selectedDate, hasDateFilter } = useDashboardScope()
|
||||||
const tableData = ref<PlayRecordItem[]>([])
|
const tableData = ref<PlayRecordItem[]>([])
|
||||||
|
|
||||||
|
const listSubtitle = computed(() =>
|
||||||
|
hasDateFilter.value && selectedDate.value
|
||||||
|
? t('console.playRecord.subtitleByDate', { date: selectedDate.value })
|
||||||
|
: t('console.playRecord.subtitle')
|
||||||
|
)
|
||||||
|
|
||||||
function formatCoin(val: number | undefined): string {
|
function formatCoin(val: number | undefined): string {
|
||||||
if (val === undefined || val === null) return '0.00'
|
if (val === undefined || val === null) return '0.00'
|
||||||
return Number(val).toFixed(2)
|
return Number(val).toFixed(2)
|
||||||
}
|
}
|
||||||
|
|
||||||
const loadList = () => {
|
const loadList = () => {
|
||||||
fetchPlayRecordList(getChannelDeptRequestParams()).then((data) => {
|
fetchPlayRecordList(queryParams.value).then((data) => {
|
||||||
tableData.value = Array.isArray(data) ? data : []
|
tableData.value = Array.isArray(data) ? data : []
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
useChannelDeptReload(loadList)
|
useDashboardReload(loadList)
|
||||||
|
|
||||||
|
const goPlayRecords = () => {
|
||||||
|
openPlayRecord(dashboardDateNavParams(selectedDate.value))
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="art-card p-5 overflow-hidden mb-5 max-sm:mb-4">
|
<div class="art-card p-5 overflow-hidden mb-5 max-sm:mb-4">
|
||||||
<div class="art-card-header mb-4">
|
<div class="art-card-header mb-4 flex items-start justify-between gap-3">
|
||||||
<div class="title">
|
<div class="title">
|
||||||
<h4>{{ $t('console.walletRecord.title') }}</h4>
|
<h4>{{ $t('console.walletRecord.title') }}</h4>
|
||||||
<p class="text-g-600 text-sm mt-1">{{ $t('console.walletRecord.subtitle') }}</p>
|
<p class="text-g-600 text-sm mt-1">{{ listSubtitle }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
<ElButton type="primary" link @click="goWalletRecords">
|
||||||
|
{{ $t('console.nav.viewAllRecharge') }}
|
||||||
|
</ElButton>
|
||||||
</div>
|
</div>
|
||||||
<ArtTable
|
<ArtTable
|
||||||
class="w-full"
|
class="w-full"
|
||||||
@@ -30,20 +33,40 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { fetchWalletRecordList, type WalletRecordItem } from '@/api/dashboard'
|
import { fetchWalletRecordList, type WalletRecordItem } from '@/api/dashboard'
|
||||||
import { getChannelDeptRequestParams, useChannelDeptReload } from '@/composables/useChannelDeptScope'
|
import { useDashboardReload, useDashboardScope } from '@/composables/useDashboardScope'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import {
|
||||||
|
dashboardDateNavParams,
|
||||||
|
openWalletRecord
|
||||||
|
} from '@/views/plugin/dice/utils/dashboardRecordNav'
|
||||||
|
|
||||||
|
const { t } = useI18n()
|
||||||
|
const { queryParams, selectedDate, hasDateFilter } = useDashboardScope()
|
||||||
const tableData = ref<WalletRecordItem[]>([])
|
const tableData = ref<WalletRecordItem[]>([])
|
||||||
|
|
||||||
|
const listSubtitle = computed(() =>
|
||||||
|
hasDateFilter.value && selectedDate.value
|
||||||
|
? t('console.walletRecord.subtitleByDate', { date: selectedDate.value })
|
||||||
|
: t('console.walletRecord.subtitle')
|
||||||
|
)
|
||||||
|
|
||||||
function formatCoin(val: number | undefined): string {
|
function formatCoin(val: number | undefined): string {
|
||||||
if (val === undefined || val === null) return '0.00'
|
if (val === undefined || val === null) return '0.00'
|
||||||
return Number(val).toFixed(2)
|
return Number(val).toFixed(2)
|
||||||
}
|
}
|
||||||
|
|
||||||
const loadList = () => {
|
const loadList = () => {
|
||||||
fetchWalletRecordList(getChannelDeptRequestParams()).then((data) => {
|
fetchWalletRecordList(queryParams.value).then((data) => {
|
||||||
tableData.value = Array.isArray(data) ? data : []
|
tableData.value = Array.isArray(data) ? data : []
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
useChannelDeptReload(loadList)
|
useDashboardReload(loadList)
|
||||||
|
|
||||||
|
const goWalletRecords = () => {
|
||||||
|
openWalletRecord({
|
||||||
|
type: 0,
|
||||||
|
...dashboardDateNavParams(selectedDate.value)
|
||||||
|
})
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -9,13 +9,19 @@
|
|||||||
>
|
>
|
||||||
<el-form ref="formRef" :model="formData" :rules="rules" label-width="120px">
|
<el-form ref="formRef" :model="formData" :rules="rules" label-width="120px">
|
||||||
<el-form-item :label="$t('page.form.labelName')" prop="name">
|
<el-form-item :label="$t('page.form.labelName')" prop="name">
|
||||||
<el-input v-model="formData.name" :placeholder="$t('page.form.placeholderName')" />
|
<el-input v-model="formData.name" disabled :placeholder="$t('page.form.placeholderNameAuto')" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item :label="$t('page.form.labelTitle')" prop="title">
|
<el-form-item :label="$t('page.form.labelTitle')" prop="title">
|
||||||
<el-input v-model="formData.title" :placeholder="$t('page.form.placeholderTitle')" />
|
<el-input v-model="formData.title" disabled :placeholder="$t('page.form.placeholderTitleAuto')" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item :label="$t('page.form.labelMult')" prop="mult">
|
<el-form-item :label="$t('page.form.labelMult')" prop="mult">
|
||||||
<el-input-number v-model="formData.mult" :min="1" :step="1" style="width: 100%" />
|
<el-input-number
|
||||||
|
v-model="formData.mult"
|
||||||
|
:min="1"
|
||||||
|
:step="1"
|
||||||
|
style="width: 100%"
|
||||||
|
@update:model-value="syncNameTitleFromMult"
|
||||||
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item :label="$t('page.form.labelIsDefault')" prop="is_default">
|
<el-form-item :label="$t('page.form.labelIsDefault')" prop="is_default">
|
||||||
<el-radio-group v-model="formData.is_default">
|
<el-radio-group v-model="formData.is_default">
|
||||||
@@ -87,6 +93,13 @@
|
|||||||
|
|
||||||
const formData = reactive({ ...initialFormData })
|
const formData = reactive({ ...initialFormData })
|
||||||
|
|
||||||
|
function syncNameTitleFromMult() {
|
||||||
|
const mult = Number(formData.mult) || 1
|
||||||
|
const label = `x${mult}`
|
||||||
|
formData.name = label
|
||||||
|
formData.title = label
|
||||||
|
}
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => props.modelValue,
|
() => props.modelValue,
|
||||||
async (newVal) => {
|
async (newVal) => {
|
||||||
@@ -99,6 +112,7 @@
|
|||||||
if (typeof props.data.title === 'string') formData.title = props.data.title
|
if (typeof props.data.title === 'string') formData.title = props.data.title
|
||||||
formData.mult = Number(props.data.mult ?? 1) || 1
|
formData.mult = Number(props.data.mult ?? 1) || 1
|
||||||
formData.is_default = Number(props.data.is_default ?? 0) === 1 ? 1 : 0
|
formData.is_default = Number(props.data.is_default ?? 0) === 1 ? 1 : 0
|
||||||
|
syncNameTitleFromMult()
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,29 @@
|
|||||||
import request from '@/utils/http'
|
import request from '@/utils/http'
|
||||||
|
import {
|
||||||
|
normalizeLotteryPoolOption,
|
||||||
|
type LotteryPoolOption
|
||||||
|
} from '@/views/plugin/dice/utils/lotteryPoolDisplay'
|
||||||
|
|
||||||
|
export type LotteryPoolConfigOption = LotteryPoolOption & {
|
||||||
|
t1_weight: number
|
||||||
|
t2_weight: number
|
||||||
|
t3_weight: number
|
||||||
|
t4_weight: number
|
||||||
|
t5_weight: number
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 规范化接口返回的彩金池配置(含权重) */
|
||||||
|
export function parseLotteryPoolConfigOption(raw: Record<string, unknown>): LotteryPoolConfigOption {
|
||||||
|
const base = normalizeLotteryPoolOption(raw)
|
||||||
|
return {
|
||||||
|
...base,
|
||||||
|
t1_weight: Number(raw.t1_weight ?? 0),
|
||||||
|
t2_weight: Number(raw.t2_weight ?? 0),
|
||||||
|
t3_weight: Number(raw.t3_weight ?? 0),
|
||||||
|
t4_weight: Number(raw.t4_weight ?? 0),
|
||||||
|
t5_weight: Number(raw.t5_weight ?? 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 色子奖池配置 API 接口
|
* 色子奖池配置 API 接口
|
||||||
@@ -20,32 +45,14 @@ export default {
|
|||||||
* 获取 DiceLotteryPoolConfig 列表数据,含 id、name、t1_weight~t5_weight,用于一键测试权重档位类型下拉
|
* 获取 DiceLotteryPoolConfig 列表数据,含 id、name、t1_weight~t5_weight,用于一键测试权重档位类型下拉
|
||||||
* name 映射:default=原 type=0,killScore=原 type=1,up=原 type=2
|
* name 映射:default=原 type=0,killScore=原 type=1,up=原 type=2
|
||||||
*/
|
*/
|
||||||
async getOptions(params?: Record<string, unknown>): Promise<
|
async getOptions(params?: Record<string, unknown>): Promise<LotteryPoolConfigOption[]> {
|
||||||
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<any>({
|
const res = await request.get<any>({
|
||||||
url: '/core/dice/lottery_pool_config/DiceLotteryPoolConfig/getOptions',
|
url: '/core/dice/lottery_pool_config/DiceLotteryPoolConfig/getOptions',
|
||||||
params
|
params
|
||||||
})
|
})
|
||||||
const rows = Array.isArray(res) ? res : (Array.isArray((res as any)?.data) ? (res as any).data : [])
|
const rows = Array.isArray(res) ? res : (Array.isArray((res as any)?.data) ? (res as any).data : [])
|
||||||
if (!Array.isArray(rows)) return []
|
if (!Array.isArray(rows)) return []
|
||||||
return rows.map((r: any) => ({
|
return rows.map((r: Record<string, unknown>) => parseLotteryPoolConfigOption(r))
|
||||||
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)
|
|
||||||
}))
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import request from '@/utils/http'
|
import request from '@/utils/http'
|
||||||
|
import { normalizeLotteryPoolOption, type LotteryPoolOption } from '@/views/plugin/dice/utils/lotteryPoolDisplay'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 玩家抽奖记录 API接口
|
* 玩家抽奖记录 API接口
|
||||||
@@ -39,18 +40,6 @@ export default {
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
|
||||||
* 更新数据
|
|
||||||
* @param params 数据参数
|
|
||||||
* @returns 执行结果
|
|
||||||
*/
|
|
||||||
update(params: Record<string, any>) {
|
|
||||||
return request.put<any>({
|
|
||||||
url: '/core/dice/play_record/DicePlayRecord/update',
|
|
||||||
data: params
|
|
||||||
})
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 删除数据
|
* 删除数据
|
||||||
* @param id 数据ID
|
* @param id 数据ID
|
||||||
@@ -71,11 +60,13 @@ export default {
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
/** 获取彩金池配置选项(id、name) */
|
/** 获取彩金池配置选项(含奖池名称) */
|
||||||
getLotteryConfigOptions(params?: Record<string, unknown>) {
|
async getLotteryConfigOptions(params?: Record<string, unknown>): Promise<LotteryPoolOption[]> {
|
||||||
return request.get<{ id: number; name: string }[]>({
|
const res = await request.get<any>({
|
||||||
url: '/core/dice/play_record/DicePlayRecord/getLotteryConfigOptions',
|
url: '/core/dice/play_record/DicePlayRecord/getLotteryConfigOptions',
|
||||||
params
|
params
|
||||||
})
|
})
|
||||||
|
const rows = (Array.isArray(res) ? res : (res?.data ?? [])) as Array<Record<string, unknown>>
|
||||||
|
return rows.map((r) => normalizeLotteryPoolOption(r))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,18 +39,6 @@ export default {
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
|
||||||
* 更新数据
|
|
||||||
* @param params 数据参数
|
|
||||||
* @returns 执行结果
|
|
||||||
*/
|
|
||||||
update(params: Record<string, any>) {
|
|
||||||
return request.put<any>({
|
|
||||||
url: '/core/dice/play_record_test/DicePlayRecordTest/update',
|
|
||||||
data: params
|
|
||||||
})
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 删除数据
|
* 删除数据
|
||||||
* @param id 数据ID
|
* @param id 数据ID
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import request from '@/utils/http'
|
import request from '@/utils/http'
|
||||||
|
import { normalizeLotteryPoolOption, type LotteryPoolOption } from '@/views/plugin/dice/utils/lotteryPoolDisplay'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 大富翁-玩家 API接口
|
* 大富翁-玩家 API接口
|
||||||
@@ -84,16 +85,15 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取彩金池配置选项(DiceLotteryPoolConfig.id、name),供 lottery_config_id 下拉使用
|
* 获取彩金池配置选项,供 lottery_config_id 下拉使用(含奖池名称 display_name)
|
||||||
* @returns [ { id, name } ]
|
|
||||||
*/
|
*/
|
||||||
async getLotteryConfigOptions(params?: Record<string, unknown>): Promise<Array<{ id: number; name: string }>> {
|
async getLotteryConfigOptions(params?: Record<string, unknown>): Promise<LotteryPoolOption[]> {
|
||||||
const res = await request.get<any>({
|
const res = await request.get<any>({
|
||||||
url: '/core/dice/player/DicePlayer/getLotteryConfigOptions',
|
url: '/core/dice/player/DicePlayer/getLotteryConfigOptions',
|
||||||
params
|
params
|
||||||
})
|
})
|
||||||
const rows = (Array.isArray(res) ? res : (res?.data ?? [])) as Array<{ id: number; name: string }>
|
const rows = (Array.isArray(res) ? res : (res?.data ?? [])) as Array<Record<string, unknown>>
|
||||||
return rows.map((r) => ({ id: Number(r.id), name: String(r.name ?? r.id ?? '') }))
|
return rows.map((r) => normalizeLotteryPoolOption(r))
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -39,18 +39,6 @@ export default {
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
|
||||||
* 更新数据
|
|
||||||
* @param params 数据参数
|
|
||||||
* @returns 执行结果
|
|
||||||
*/
|
|
||||||
update(params: Record<string, any>) {
|
|
||||||
return request.put<any>({
|
|
||||||
url: '/core/dice/player_ticket_record/DicePlayerTicketRecord/update',
|
|
||||||
data: params
|
|
||||||
})
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 删除数据
|
* 删除数据
|
||||||
* @param id 数据ID
|
* @param id 数据ID
|
||||||
|
|||||||
@@ -10,7 +10,11 @@ export default {
|
|||||||
* @returns 数据列表
|
* @returns 数据列表
|
||||||
*/
|
*/
|
||||||
list(params: Record<string, any>) {
|
list(params: Record<string, any>) {
|
||||||
return request.get<Api.Common.ApiPage & { total_coin_change?: number }>({
|
return request.get<Api.Common.ApiPage & {
|
||||||
|
total_coin_change?: number
|
||||||
|
total_coin_inflow?: number
|
||||||
|
total_coin_outflow?: number
|
||||||
|
}>({
|
||||||
url: '/core/dice/player_wallet_record/DicePlayerWalletRecord/index',
|
url: '/core/dice/player_wallet_record/DicePlayerWalletRecord/index',
|
||||||
params
|
params
|
||||||
})
|
})
|
||||||
@@ -39,18 +43,6 @@ export default {
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
|
||||||
* 更新数据
|
|
||||||
* @param params 数据参数
|
|
||||||
* @returns 执行结果
|
|
||||||
*/
|
|
||||||
update(params: Record<string, any>) {
|
|
||||||
return request.put<any>({
|
|
||||||
url: '/core/dice/player_wallet_record/DicePlayerWalletRecord/update',
|
|
||||||
data: params
|
|
||||||
})
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 删除数据
|
* 删除数据
|
||||||
* @param id 数据ID
|
* @param id 数据ID
|
||||||
|
|||||||
@@ -76,6 +76,27 @@ export default {
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按规则生成并保存奖励索引(需 dice:reward_config:index:tierRecommend 权限)
|
||||||
|
*/
|
||||||
|
generateIndexByRules(
|
||||||
|
items: Array<{
|
||||||
|
id: number
|
||||||
|
grid_number?: number
|
||||||
|
ui_text?: string
|
||||||
|
ui_text_en?: string
|
||||||
|
real_ev?: number
|
||||||
|
tier?: string
|
||||||
|
remark?: string
|
||||||
|
}>,
|
||||||
|
extra?: Record<string, any>
|
||||||
|
) {
|
||||||
|
return request.post<any>({
|
||||||
|
url: '/core/dice/reward_config/DiceRewardConfig/generateIndexByRules',
|
||||||
|
data: { items, ...(extra || {}) }
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* T1-T5、BIGWIN 权重配比:按档位分组获取配置列表
|
* T1-T5、BIGWIN 权重配比:按档位分组获取配置列表
|
||||||
*/
|
*/
|
||||||
@@ -126,5 +147,15 @@ export default {
|
|||||||
url: '/core/dice/reward_config/DiceRewardConfig/createRewardReference',
|
url: '/core/dice/reward_config/DiceRewardConfig/createRewardReference',
|
||||||
data: params || {}
|
data: params || {}
|
||||||
})
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建奖励对照(预览):不写库,返回将要生成的对照与权重预览
|
||||||
|
*/
|
||||||
|
createRewardReferencePreview(params?: Record<string, any>) {
|
||||||
|
return request.post<any>({
|
||||||
|
url: '/core/dice/reward_config/DiceRewardConfig/createRewardReferencePreview',
|
||||||
|
data: params || {}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,18 +39,6 @@ export default {
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
|
||||||
* 更新数据
|
|
||||||
* @param params 数据参数
|
|
||||||
* @returns 执行结果
|
|
||||||
*/
|
|
||||||
update(params: Record<string, any>) {
|
|
||||||
return request.put<any>({
|
|
||||||
url: '/core/dice/reward_config_record/DiceRewardConfigRecord/update',
|
|
||||||
data: params
|
|
||||||
})
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 删除数据
|
* 删除数据
|
||||||
* @param id 数据ID
|
* @param id 数据ID
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
<template>
|
||||||
|
<div class="quick-date-range-bar">
|
||||||
|
<span class="quick-date-label">{{ $t('table.searchBar.quickDate') }}</span>
|
||||||
|
<ElButton
|
||||||
|
v-for="item in presetItems"
|
||||||
|
:key="item.key"
|
||||||
|
size="small"
|
||||||
|
:type="modelValue === item.key ? 'primary' : 'default'"
|
||||||
|
@click="selectPreset(item.key)"
|
||||||
|
>
|
||||||
|
{{ item.label }}
|
||||||
|
</ElButton>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import {
|
||||||
|
type DatePresetKey,
|
||||||
|
getCreateTimeRangeByPreset
|
||||||
|
} from '@/views/plugin/dice/utils/dateRangePresets'
|
||||||
|
|
||||||
|
const modelValue = defineModel<DatePresetKey | null>({ default: null })
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
select: [range: [string, string], preset: DatePresetKey]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const { t } = useI18n()
|
||||||
|
|
||||||
|
const presetItems = computed(() => [
|
||||||
|
{ key: 'today' as const, label: t('table.searchBar.quickToday') },
|
||||||
|
{ key: 'yesterday' as const, label: t('table.searchBar.quickYesterday') },
|
||||||
|
{ key: 'last7days' as const, label: t('table.searchBar.quickLast7Days') }
|
||||||
|
])
|
||||||
|
|
||||||
|
const selectPreset = (preset: DatePresetKey) => {
|
||||||
|
modelValue.value = preset
|
||||||
|
emit('select', getCreateTimeRangeByPreset(preset), preset)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.quick-date-range-bar {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-date-label {
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--el-text-color-regular);
|
||||||
|
margin-right: 4px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
import type { Ref } from 'vue'
|
||||||
|
import { useRoute } from 'vue-router'
|
||||||
|
import type { LocationQuery } from 'vue-router'
|
||||||
|
import {
|
||||||
|
detectPresetFromRange,
|
||||||
|
type DatePresetKey,
|
||||||
|
hasRecordRouteFilter,
|
||||||
|
parseRecordRouteQuery
|
||||||
|
} from '@/views/plugin/dice/utils/dateRangePresets'
|
||||||
|
|
||||||
|
export interface RecordRouteInit {
|
||||||
|
hasFilter: boolean
|
||||||
|
create_time?: [string, string]
|
||||||
|
type?: number
|
||||||
|
activePreset: DatePresetKey | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getRecordRouteInit(query: LocationQuery, withType = false): RecordRouteInit {
|
||||||
|
const filter = parseRecordRouteQuery(query)
|
||||||
|
const create_time = filter.create_time
|
||||||
|
const activePreset = filter.datePreset ?? detectPresetFromRange(create_time ?? null)
|
||||||
|
return {
|
||||||
|
hasFilter: hasRecordRouteFilter(query),
|
||||||
|
create_time,
|
||||||
|
type: withType ? filter.type : undefined,
|
||||||
|
activePreset
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 将路由 query 同步到搜索表单 */
|
||||||
|
export function syncSearchFormFromRoute(
|
||||||
|
query: LocationQuery,
|
||||||
|
searchForm: Ref<Record<string, unknown>>,
|
||||||
|
activeDatePreset: Ref<DatePresetKey | null>,
|
||||||
|
options?: { withType?: boolean }
|
||||||
|
): boolean {
|
||||||
|
const init = getRecordRouteInit(query, options?.withType)
|
||||||
|
if (init.create_time) {
|
||||||
|
searchForm.value.create_time = init.create_time
|
||||||
|
activeDatePreset.value = init.activePreset
|
||||||
|
} else {
|
||||||
|
searchForm.value.create_time = undefined
|
||||||
|
activeDatePreset.value = null
|
||||||
|
}
|
||||||
|
if (options?.withType) {
|
||||||
|
searchForm.value.type = init.type !== undefined ? init.type : undefined
|
||||||
|
}
|
||||||
|
return init.hasFilter
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildRecordRouteQueryKey(query: LocationQuery, withType = false): string {
|
||||||
|
const parts = [String(query.date ?? ''), String(query.datePreset ?? '')]
|
||||||
|
if (withType) {
|
||||||
|
parts.push(String(query.type ?? ''))
|
||||||
|
}
|
||||||
|
return parts.join('|')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 监听路由 query 变化并触发查询(解决 KeepAlive 下跳转带参不刷新的问题)
|
||||||
|
*/
|
||||||
|
export function useRecordRouteSync(options: {
|
||||||
|
searchForm: Ref<Record<string, unknown>>
|
||||||
|
activeDatePreset: Ref<DatePresetKey | null>
|
||||||
|
onSearch: (params: Record<string, unknown>) => void
|
||||||
|
withType?: boolean
|
||||||
|
skipImmediateTableLoad?: boolean
|
||||||
|
}) {
|
||||||
|
const route = useRoute()
|
||||||
|
const routeInit = getRecordRouteInit(route.query, options.withType)
|
||||||
|
let lastQueryKey = buildRecordRouteQueryKey(route.query, options.withType)
|
||||||
|
|
||||||
|
const applyRoute = () => {
|
||||||
|
const hasFilter = syncSearchFormFromRoute(
|
||||||
|
route.query,
|
||||||
|
options.searchForm,
|
||||||
|
options.activeDatePreset,
|
||||||
|
{ withType: options.withType }
|
||||||
|
)
|
||||||
|
if (hasFilter) {
|
||||||
|
options.onSearch({ ...options.searchForm.value })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (routeInit.hasFilter) {
|
||||||
|
applyRoute()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => buildRecordRouteQueryKey(route.query, options.withType),
|
||||||
|
(newKey) => {
|
||||||
|
if (newKey === lastQueryKey) return
|
||||||
|
lastQueryKey = newKey
|
||||||
|
applyRoute()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
onActivated(() => {
|
||||||
|
const key = buildRecordRouteQueryKey(route.query, options.withType)
|
||||||
|
if (key !== lastQueryKey) {
|
||||||
|
lastQueryKey = key
|
||||||
|
applyRoute()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
routeInit,
|
||||||
|
skipImmediateTableLoad: options.skipImmediateTableLoad ?? routeInit.hasFilter
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,7 +10,7 @@
|
|||||||
<template #icon>
|
<template #icon>
|
||||||
<ArtSvgIcon icon="ri:add-fill" />
|
<ArtSvgIcon icon="ri:add-fill" />
|
||||||
</template>
|
</template>
|
||||||
新增
|
{{ $t('table.actions.add') }}
|
||||||
</ElButton>
|
</ElButton>
|
||||||
<ElButton
|
<ElButton
|
||||||
v-permission="'dice:game:index:destroy'"
|
v-permission="'dice:game:index:destroy'"
|
||||||
@@ -21,7 +21,7 @@
|
|||||||
<template #icon>
|
<template #icon>
|
||||||
<ArtSvgIcon icon="ri:delete-bin-5-line" />
|
<ArtSvgIcon icon="ri:delete-bin-5-line" />
|
||||||
</template>
|
</template>
|
||||||
删除
|
{{ $t('table.actions.delete') }}
|
||||||
</ElButton>
|
</ElButton>
|
||||||
</ElSpace>
|
</ElSpace>
|
||||||
</template>
|
</template>
|
||||||
@@ -108,20 +108,20 @@
|
|||||||
apiParams: { limit: 100 },
|
apiParams: { limit: 100 },
|
||||||
columnsFactory: () => [
|
columnsFactory: () => [
|
||||||
{ type: 'selection', align: 'center' },
|
{ type: 'selection', align: 'center' },
|
||||||
{ prop: 'id', label: 'ID', width: 80, align: 'center' },
|
{ prop: 'id', label: 'page.table.id', width: 80, align: 'center' },
|
||||||
{ prop: 'provider', label: '供应商', minWidth: 120, align: 'center' },
|
{ prop: 'provider', label: 'page.table.provider', minWidth: 120, align: 'center' },
|
||||||
{ prop: 'provider_code', label: '供应商编码', minWidth: 120, align: 'center' },
|
{ prop: 'provider_code', label: 'page.table.providerCode', minWidth: 120, align: 'center' },
|
||||||
{ prop: 'game_code', label: '游戏编号', minWidth: 120, align: 'center' },
|
{ prop: 'game_code', label: 'page.table.gameCode', minWidth: 120, align: 'center' },
|
||||||
{ prop: 'game_key', label: '游戏唯一值', minWidth: 120, align: 'center' },
|
{ prop: 'game_key', label: 'page.table.gameKey', minWidth: 120, align: 'center' },
|
||||||
{ prop: 'game_name', label: '中文名', minWidth: 120, align: 'center' },
|
{ prop: 'game_name', label: 'page.table.gameName', minWidth: 120, align: 'center' },
|
||||||
{ prop: 'game_name_en', label: '英文名', minWidth: 120, align: 'center' },
|
{ prop: 'game_name_en', label: 'page.table.gameNameEn', minWidth: 120, align: 'center' },
|
||||||
{ prop: 'game_type', label: '类型', minWidth: 90, align: 'center' },
|
{ prop: 'game_type', label: 'page.table.gameType', minWidth: 90, align: 'center' },
|
||||||
{ prop: 'sort', label: '排序', width: 80, align: 'center' },
|
{ prop: 'sort', label: 'page.table.sort', width: 80, align: 'center' },
|
||||||
{ prop: 'status', label: '状态', width: 90, align: 'center', useSlot: true },
|
{ prop: 'status', label: 'page.table.status', width: 90, align: 'center', useSlot: true },
|
||||||
{ prop: 'update_time', label: '更新时间', minWidth: 160, align: 'center' },
|
{ prop: 'update_time', label: 'page.table.updateTime', minWidth: 160, align: 'center' },
|
||||||
{
|
{
|
||||||
prop: 'operation',
|
prop: 'operation',
|
||||||
label: '操作',
|
label: 'table.actions.operation',
|
||||||
width: 110,
|
width: 110,
|
||||||
align: 'center',
|
align: 'center',
|
||||||
fixed: 'right',
|
fixed: 'right',
|
||||||
|
|||||||
@@ -8,25 +8,41 @@
|
|||||||
@search="handleSearch"
|
@search="handleSearch"
|
||||||
>
|
>
|
||||||
<el-col v-bind="setSpan(6)">
|
<el-col v-bind="setSpan(6)">
|
||||||
<el-form-item label="供应商编码" prop="provider_code">
|
<el-form-item :label="$t('page.search.providerCode')" prop="provider_code">
|
||||||
<el-input v-model="formData.provider_code" placeholder="请输入供应商编码" clearable />
|
<el-input
|
||||||
|
v-model="formData.provider_code"
|
||||||
|
:placeholder="$t('page.search.placeholderProviderCode')"
|
||||||
|
clearable
|
||||||
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col v-bind="setSpan(6)">
|
<el-col v-bind="setSpan(6)">
|
||||||
<el-form-item label="游戏编号" prop="game_code">
|
<el-form-item :label="$t('page.search.gameCode')" prop="game_code">
|
||||||
<el-input v-model="formData.game_code" placeholder="请输入游戏编号" clearable />
|
<el-input
|
||||||
|
v-model="formData.game_code"
|
||||||
|
:placeholder="$t('page.search.placeholderGameCode')"
|
||||||
|
clearable
|
||||||
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col v-bind="setSpan(6)">
|
<el-col v-bind="setSpan(6)">
|
||||||
<el-form-item label="游戏类型" prop="game_type">
|
<el-form-item :label="$t('page.search.gameType')" prop="game_type">
|
||||||
<el-input v-model="formData.game_type" placeholder="请输入游戏类型" clearable />
|
<el-input
|
||||||
|
v-model="formData.game_type"
|
||||||
|
:placeholder="$t('page.search.placeholderGameType')"
|
||||||
|
clearable
|
||||||
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col v-bind="setSpan(6)">
|
<el-col v-bind="setSpan(6)">
|
||||||
<el-form-item label="状态" prop="status">
|
<el-form-item :label="$t('page.search.status')" prop="status">
|
||||||
<el-select v-model="formData.status" placeholder="请选择状态" clearable>
|
<el-select
|
||||||
<el-option label="启用" :value="1" />
|
v-model="formData.status"
|
||||||
<el-option label="禁用" :value="0" />
|
:placeholder="$t('page.search.placeholderStatus')"
|
||||||
|
clearable
|
||||||
|
>
|
||||||
|
<el-option :label="$t('page.search.statusEnabled')" :value="1" />
|
||||||
|
<el-option :label="$t('page.search.statusDisabled')" :value="0" />
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
|
|||||||
@@ -29,6 +29,12 @@
|
|||||||
@pagination:size-change="handleSizeChange"
|
@pagination:size-change="handleSizeChange"
|
||||||
@pagination:current-change="handleCurrentChange"
|
@pagination:current-change="handleCurrentChange"
|
||||||
>
|
>
|
||||||
|
<template #safety_line="{ row }">
|
||||||
|
<span v-if="isDefaultPoolRow(row)" class="font-mono">{{
|
||||||
|
formatSafetyLine(row.safety_line)
|
||||||
|
}}</span>
|
||||||
|
<span v-else class="text-gray-400 text-xs">{{ $t('page.table.safetyLineNotUsed') }}</span>
|
||||||
|
</template>
|
||||||
<!-- 操作列 -->
|
<!-- 操作列 -->
|
||||||
<template #operation="{ row }">
|
<template #operation="{ row }">
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
@@ -81,13 +87,10 @@
|
|||||||
getData()
|
getData()
|
||||||
}
|
}
|
||||||
|
|
||||||
// 奖池类型展示:按 name 映射
|
const poolNameFormatter = (row: Record<string, unknown>) => {
|
||||||
const typeFormatter = (row: Record<string, unknown>) => {
|
const remark = String(row.remark ?? '').trim()
|
||||||
const n = String(row.name ?? '')
|
if (remark) return remark
|
||||||
if (n === 'default') return t('page.search.poolTypeNormal')
|
return String(row.name ?? '').trim() || '-'
|
||||||
if (n === 'killScore') return t('page.search.poolTypeKill')
|
|
||||||
if (n === 'up') return t('page.search.poolTypeT1')
|
|
||||||
return n || '-'
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 权重列带 %
|
// 权重列带 %
|
||||||
@@ -96,6 +99,17 @@
|
|||||||
return v != null && v !== '' ? `${v}%` : '-'
|
return v != null && v !== '' ? `${v}%` : '-'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 仅 name=default(正常)奖池的安全线参与杀分判定 */
|
||||||
|
function isDefaultPoolRow(row: Record<string, unknown>): boolean {
|
||||||
|
return String(row.name ?? '') === 'default'
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatSafetyLine(val: unknown): string {
|
||||||
|
if (val === null || val === undefined || val === '') return '-'
|
||||||
|
const n = typeof val === 'number' ? val : Number(val)
|
||||||
|
return Number.isFinite(n) ? n.toFixed(2) : '-'
|
||||||
|
}
|
||||||
|
|
||||||
// 表格配置
|
// 表格配置
|
||||||
const {
|
const {
|
||||||
columns,
|
columns,
|
||||||
@@ -114,9 +128,26 @@
|
|||||||
core: {
|
core: {
|
||||||
apiFn: api.list,
|
apiFn: api.list,
|
||||||
columnsFactory: () => [
|
columnsFactory: () => [
|
||||||
{ prop: 'name', label: 'page.table.name', align: 'center' },
|
{ prop: 'remark', label: 'page.table.poolName', minWidth: 120, align: 'center', formatter: poolNameFormatter },
|
||||||
{ prop: 'name', label: 'page.table.poolType', width: 100, align: 'center', formatter: typeFormatter },
|
{
|
||||||
{ prop: 'safety_line', label: 'page.table.safetyLine', align: 'center' },
|
prop: 'config_note',
|
||||||
|
label: 'page.table.configNote',
|
||||||
|
minWidth: 140,
|
||||||
|
align: 'center',
|
||||||
|
showOverflowTooltip: true,
|
||||||
|
formatter: (row: Record<string, unknown>) => {
|
||||||
|
const v = String(row.config_note ?? '').trim()
|
||||||
|
return v || '-'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ prop: 'name', label: 'page.table.name', width: 110, align: 'center' },
|
||||||
|
{
|
||||||
|
prop: 'safety_line',
|
||||||
|
label: 'page.table.safetyLine',
|
||||||
|
minWidth: 120,
|
||||||
|
align: 'center',
|
||||||
|
useSlot: true
|
||||||
|
},
|
||||||
{
|
{
|
||||||
prop: 't1_weight',
|
prop: 't1_weight',
|
||||||
label: 'page.table.t1PoolWeight',
|
label: 'page.table.t1PoolWeight',
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="profit-row mb-3">
|
<div class="profit-row mb-3">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<span class="text-gray-500">{{ $t('page.form.playerProfit') }}</span>
|
<span class="text-gray-500">{{ $t('page.form.poolProfitAmount') }}</span>
|
||||||
<span class="font-mono text-lg" :class="profitAmountClass">{{
|
<span class="font-mono text-lg" :class="profitAmountClass">{{
|
||||||
displayProfitAmount
|
displayProfitAmount
|
||||||
}}</span>
|
}}</span>
|
||||||
@@ -41,6 +41,7 @@
|
|||||||
:precision="2"
|
:precision="2"
|
||||||
style="width: 100%"
|
style="width: 100%"
|
||||||
/>
|
/>
|
||||||
|
<div class="text-gray-500 text-xs mt-1">{{ $t('page.table.safetyLineTip') }}</div>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item :label="$t('page.form.enableKillScore')">
|
<el-form-item :label="$t('page.form.enableKillScore')">
|
||||||
<el-switch v-model="formData.kill_enabled" :active-value="1" :inactive-value="0" />
|
<el-switch v-model="formData.kill_enabled" :active-value="1" :inactive-value="0" />
|
||||||
@@ -239,6 +240,8 @@
|
|||||||
() => props.modelValue,
|
() => props.modelValue,
|
||||||
(open) => {
|
(open) => {
|
||||||
if (open) {
|
if (open) {
|
||||||
|
// 切换渠道后再打开弹窗时,先清掉旧池数据,避免视觉残留
|
||||||
|
pool.value = null
|
||||||
loadPool().then(() => startPolling())
|
loadPool().then(() => startPolling())
|
||||||
} else {
|
} else {
|
||||||
stopPolling()
|
stopPolling()
|
||||||
|
|||||||
@@ -15,24 +15,41 @@
|
|||||||
:disabled="dialogType === 'edit'"
|
:disabled="dialogType === 'edit'"
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item :label="$t('form.labelRemark')" prop="remark">
|
<el-form-item :label="$t('page.form.poolName')" prop="remark">
|
||||||
<el-input
|
<el-input
|
||||||
v-model="formData.remark"
|
v-model="formData.remark"
|
||||||
|
:placeholder="$t('page.form.placeholderPoolName')"
|
||||||
|
maxlength="200"
|
||||||
|
show-word-limit
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item :label="$t('form.labelRemark')" prop="config_note">
|
||||||
|
<el-input
|
||||||
|
v-model="formData.config_note"
|
||||||
type="textarea"
|
type="textarea"
|
||||||
:rows="3"
|
:rows="3"
|
||||||
:placeholder="$t('page.form.placeholderRemark')"
|
:placeholder="$t('page.form.placeholderConfigNote')"
|
||||||
maxlength="500"
|
maxlength="500"
|
||||||
show-word-limit
|
show-word-limit
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<!-- dice_lottery_pool_config 已移除 type 字段,按 name 区分 default/killScore/up;name 在新增时填写,编辑时禁用 -->
|
<!-- dice_lottery_pool_config 已移除 type 字段,按 name 区分 default/killScore/up;name 在新增时填写,编辑时禁用 -->
|
||||||
<el-form-item :label="$t('page.form.safetyLine')" prop="safety_line">
|
<el-form-item v-if="showSafetyLineField" :label="$t('page.form.safetyLine')" prop="safety_line">
|
||||||
<el-input-number
|
<el-input-number
|
||||||
v-model="formData.safety_line"
|
v-model="formData.safety_line"
|
||||||
:min="0"
|
:min="0"
|
||||||
:precision="2"
|
:precision="2"
|
||||||
style="width: 100%"
|
style="width: 100%"
|
||||||
/>
|
/>
|
||||||
|
<div class="text-gray-500 text-xs mt-1">{{ $t('page.table.safetyLineTip') }}</div>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item v-else-if="showSafetyLineReadonlyHint" :label="$t('page.form.safetyLine')">
|
||||||
|
<el-alert
|
||||||
|
type="info"
|
||||||
|
:closable="false"
|
||||||
|
show-icon
|
||||||
|
:title="$t('page.form.safetyLineNotUsedReadonly')"
|
||||||
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item :label="$t('page.form.t1Weight')" prop="t1_weight">
|
<el-form-item :label="$t('page.form.t1Weight')" prop="t1_weight">
|
||||||
<el-slider v-model="formData.t1_weight" :min="0" :max="100" :step="0.01" show-input />
|
<el-slider v-model="formData.t1_weight" :min="0" :max="100" :step="0.01" show-input />
|
||||||
@@ -111,17 +128,36 @@
|
|||||||
return WEIGHT_KEYS.reduce((sum, key) => sum + Number(formData[key] ?? 0), 0)
|
return WEIGHT_KEYS.reduce((sum, key) => sum + Number(formData[key] ?? 0), 0)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
function isDefaultPoolName(name: unknown): boolean {
|
||||||
|
return String(name ?? '') === 'default'
|
||||||
|
}
|
||||||
|
|
||||||
|
const showSafetyLineField = computed(() => isDefaultPoolName(formData.name))
|
||||||
|
|
||||||
|
const showSafetyLineReadonlyHint = computed(() => {
|
||||||
|
const n = String(formData.name ?? '').trim()
|
||||||
|
return n !== '' && !isDefaultPoolName(n)
|
||||||
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 表单验证规则
|
* 表单验证规则
|
||||||
*/
|
*/
|
||||||
const rules = computed<FormRules>(() => ({
|
const rules = computed<FormRules>(() => {
|
||||||
|
const base: FormRules = {
|
||||||
name: [{ required: true, message: t('page.form.ruleNameRequired'), trigger: 'blur' }],
|
name: [{ required: true, message: t('page.form.ruleNameRequired'), trigger: 'blur' }],
|
||||||
t1_weight: [{ required: true, message: t('page.form.ruleT1Required'), trigger: 'blur' }],
|
t1_weight: [{ required: true, message: t('page.form.ruleT1Required'), trigger: 'blur' }],
|
||||||
t2_weight: [{ required: true, message: t('page.form.ruleT2Required'), trigger: 'blur' }],
|
t2_weight: [{ required: true, message: t('page.form.ruleT2Required'), trigger: 'blur' }],
|
||||||
t3_weight: [{ required: true, message: t('page.form.ruleT3Required'), trigger: 'blur' }],
|
t3_weight: [{ required: true, message: t('page.form.ruleT3Required'), trigger: 'blur' }],
|
||||||
t4_weight: [{ required: true, message: t('page.form.ruleT4Required'), trigger: 'blur' }],
|
t4_weight: [{ required: true, message: t('page.form.ruleT4Required'), trigger: 'blur' }],
|
||||||
t5_weight: [{ required: true, message: t('page.form.ruleT5Required'), trigger: 'blur' }]
|
t5_weight: [{ required: true, message: t('page.form.ruleT5Required'), trigger: 'blur' }]
|
||||||
}))
|
}
|
||||||
|
if (showSafetyLineField.value) {
|
||||||
|
base.safety_line = [
|
||||||
|
{ required: true, message: t('page.form.ruleSafetyLineRequired'), trigger: 'blur' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
return base
|
||||||
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 初始数据(权重为数字便于输入与校验)
|
* 初始数据(权重为数字便于输入与校验)
|
||||||
@@ -131,6 +167,7 @@
|
|||||||
dept_id: undefined as number | undefined,
|
dept_id: undefined as number | undefined,
|
||||||
name: '',
|
name: '',
|
||||||
remark: '',
|
remark: '',
|
||||||
|
config_note: '',
|
||||||
safety_line: 0 as number,
|
safety_line: 0 as number,
|
||||||
t1_weight: 0 as number,
|
t1_weight: 0 as number,
|
||||||
t2_weight: 0 as number,
|
t2_weight: 0 as number,
|
||||||
@@ -224,11 +261,15 @@
|
|||||||
props.data?.dept_id ??
|
props.data?.dept_id ??
|
||||||
channelScope?.selectedDeptId.value
|
channelScope?.selectedDeptId.value
|
||||||
})
|
})
|
||||||
|
const { safety_line, ...submitWithoutSafetyLine } = submitData
|
||||||
|
const payload = isDefaultPoolName(submitData.name)
|
||||||
|
? submitData
|
||||||
|
: submitWithoutSafetyLine
|
||||||
if (props.dialogType === 'add') {
|
if (props.dialogType === 'add') {
|
||||||
await api.save(submitData)
|
await api.save(payload)
|
||||||
ElMessage.success(t('page.form.msgAddSuccess'))
|
ElMessage.success(t('page.form.msgAddSuccess'))
|
||||||
} else {
|
} else {
|
||||||
await api.update(submitData)
|
await api.update(payload)
|
||||||
ElMessage.success(t('page.form.msgUpdateSuccess'))
|
ElMessage.success(t('page.form.msgUpdateSuccess'))
|
||||||
}
|
}
|
||||||
emit('success')
|
emit('success')
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="art-full-height">
|
<div class="art-full-height">
|
||||||
|
<QuickDateRangeBar v-model="activeDatePreset" @select="handleQuickDateSelect" />
|
||||||
<!-- 搜索面板 -->
|
<!-- 搜索面板 -->
|
||||||
<TableSearch v-model="searchForm" @search="handleSearch" @reset="resetSearchParams" />
|
<TableSearch v-model="searchForm" @search="handleSearch" @reset="handleResetSearch" />
|
||||||
|
|
||||||
<ElCard class="art-table-card" shadow="never">
|
<ElCard class="art-table-card" shadow="never">
|
||||||
<!-- 表格头部 -->
|
<!-- 表格头部 -->
|
||||||
@@ -138,12 +139,24 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import { useRoute } from 'vue-router'
|
||||||
import { useTable } from '@/hooks/core/useTable'
|
import { useTable } from '@/hooks/core/useTable'
|
||||||
import { useSaiAdmin } from '@/composables/useSaiAdmin'
|
import { useSaiAdmin } from '@/composables/useSaiAdmin'
|
||||||
import api from '../../api/play_record/index'
|
import api from '../../api/play_record/index'
|
||||||
import TableSearch from './modules/table-search.vue'
|
import TableSearch from './modules/table-search.vue'
|
||||||
import EditDialog from './modules/edit-dialog.vue'
|
import EditDialog from './modules/edit-dialog.vue'
|
||||||
|
import { lotteryPoolRowLabel } from '@/views/plugin/dice/utils/lotteryPoolDisplay'
|
||||||
|
import QuickDateRangeBar from '@/views/plugin/dice/components/QuickDateRangeBar.vue'
|
||||||
|
import {
|
||||||
|
detectPresetFromRange,
|
||||||
|
type DatePresetKey
|
||||||
|
} from '@/views/plugin/dice/utils/dateRangePresets'
|
||||||
|
import { getRecordRouteInit, useRecordRouteSync } from '@/views/plugin/dice/composables/useRecordRouteQuery'
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
|
const route = useRoute()
|
||||||
|
const routeInit = getRecordRouteInit(route.query)
|
||||||
|
const activeDatePreset = ref<DatePresetKey | null>(routeInit.activePreset)
|
||||||
|
|
||||||
// 搜索表单
|
// 搜索表单
|
||||||
const searchForm = ref<Record<string, unknown>>({
|
const searchForm = ref<Record<string, unknown>>({
|
||||||
@@ -157,9 +170,40 @@
|
|||||||
roll_number_max: undefined,
|
roll_number_max: undefined,
|
||||||
reward_ui_text: undefined,
|
reward_ui_text: undefined,
|
||||||
reward_tier: undefined,
|
reward_tier: undefined,
|
||||||
direction: undefined
|
direction: undefined,
|
||||||
|
create_time: routeInit.create_time
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const PLAY_RECORD_SEARCH_KEYS = [
|
||||||
|
'username',
|
||||||
|
'lottery_config_name',
|
||||||
|
'lottery_type',
|
||||||
|
'is_win',
|
||||||
|
'win_coin_min',
|
||||||
|
'win_coin_max',
|
||||||
|
'roll_number_min',
|
||||||
|
'roll_number_max',
|
||||||
|
'reward_ui_text',
|
||||||
|
'reward_tier',
|
||||||
|
'direction',
|
||||||
|
'create_time_min',
|
||||||
|
'create_time_max'
|
||||||
|
] as const
|
||||||
|
|
||||||
|
const applySearchParams = (params: Record<string, unknown>) => {
|
||||||
|
const p = { ...params }
|
||||||
|
if (Array.isArray(p.create_time) && p.create_time.length === 2) {
|
||||||
|
p.create_time_min = p.create_time[0]
|
||||||
|
p.create_time_max = p.create_time[1]
|
||||||
|
}
|
||||||
|
delete p.create_time
|
||||||
|
const paramsRecord = searchParams as Record<string, unknown>
|
||||||
|
PLAY_RECORD_SEARCH_KEYS.forEach((key) => {
|
||||||
|
delete paramsRecord[key]
|
||||||
|
})
|
||||||
|
Object.assign(searchParams, p)
|
||||||
|
}
|
||||||
|
|
||||||
/** 当前筛选下平台总盈利(付费金额 paid_amount 求和 - 玩家总收益) */
|
/** 当前筛选下平台总盈利(付费金额 paid_amount 求和 - 玩家总收益) */
|
||||||
const totalWinCoin = ref<number | null>(null)
|
const totalWinCoin = ref<number | null>(null)
|
||||||
|
|
||||||
@@ -169,16 +213,37 @@
|
|||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
|
|
||||||
// 搜索处理
|
|
||||||
const handleSearch = (params: Record<string, any>) => {
|
const handleSearch = (params: Record<string, any>) => {
|
||||||
Object.assign(searchParams, params)
|
applySearchParams(params)
|
||||||
getData()
|
getData()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleQuickDateSelect = (range: [string, string], preset: DatePresetKey) => {
|
||||||
|
searchForm.value.create_time = range
|
||||||
|
activeDatePreset.value = preset
|
||||||
|
handleSearch({ ...searchForm.value })
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleResetSearch = () => {
|
||||||
|
activeDatePreset.value = null
|
||||||
|
searchForm.value.create_time = undefined
|
||||||
|
resetSearchParams()
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => searchForm.value.create_time,
|
||||||
|
(range) => {
|
||||||
|
if (Array.isArray(range) && range.length === 2) {
|
||||||
|
activeDatePreset.value = detectPresetFromRange([range[0], range[1]])
|
||||||
|
} else {
|
||||||
|
activeDatePreset.value = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
const usernameFormatter = (row: Record<string, any>) =>
|
const usernameFormatter = (row: Record<string, any>) =>
|
||||||
row?.dicePlayer?.username ?? row?.player_id ?? '-'
|
row?.dicePlayer?.username ?? row?.player_id ?? '-'
|
||||||
const lotteryConfigNameFormatter = (row: Record<string, any>) =>
|
const lotteryConfigNameFormatter = (row: Record<string, any>) => lotteryPoolRowLabel(row)
|
||||||
row?.diceLotteryPoolConfig?.name ?? row?.lottery_config_id ?? '-'
|
|
||||||
const rewardTierFormatter = (row: Record<string, any>) => row?.reward_tier ?? '-'
|
const rewardTierFormatter = (row: Record<string, any>) => row?.reward_tier ?? '-'
|
||||||
|
|
||||||
/** 摇取点数格式化为 1,3,4,5,6,6 */
|
/** 摇取点数格式化为 1,3,4,5,6,6 */
|
||||||
@@ -221,6 +286,8 @@
|
|||||||
core: {
|
core: {
|
||||||
apiFn: listApi,
|
apiFn: listApi,
|
||||||
apiParams: { limit: 100 },
|
apiParams: { limit: 100 },
|
||||||
|
excludeParams: ['create_time'],
|
||||||
|
immediate: !routeInit.hasFilter,
|
||||||
columnsFactory: () => [
|
columnsFactory: () => [
|
||||||
// { type: 'selection' },
|
// { type: 'selection' },
|
||||||
{ prop: 'id', label: 'page.table.id', width: 80 },
|
{ prop: 'id', label: 'page.table.id', width: 80 },
|
||||||
@@ -253,6 +320,13 @@
|
|||||||
width: 100,
|
width: 100,
|
||||||
formatter: (row: Record<string, any>) => rewardTierFormatter(row)
|
formatter: (row: Record<string, any>) => rewardTierFormatter(row)
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
prop: 'remark',
|
||||||
|
label: 'page.table.remark',
|
||||||
|
width: 200,
|
||||||
|
showOverflowTooltip: true,
|
||||||
|
formatter: (row: Record<string, any>) => row?.remark || '-'
|
||||||
|
},
|
||||||
{ prop: 'create_time', label: 'page.table.createTime', width: 170 },
|
{ prop: 'create_time', label: 'page.table.createTime', width: 170 },
|
||||||
{ prop: 'update_time', label: 'page.table.updateTime', width: 170 },
|
{ prop: 'update_time', label: 'page.table.updateTime', width: 170 },
|
||||||
{
|
{
|
||||||
@@ -277,6 +351,12 @@
|
|||||||
handleSelectionChange
|
handleSelectionChange
|
||||||
// selectedRows
|
// selectedRows
|
||||||
} = useSaiAdmin()
|
} = useSaiAdmin()
|
||||||
|
|
||||||
|
useRecordRouteSync({
|
||||||
|
searchForm,
|
||||||
|
activeDatePreset,
|
||||||
|
onSearch: handleSearch
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
<template>
|
<template>
|
||||||
<el-dialog
|
<el-dialog
|
||||||
v-model="visible"
|
v-model="visible"
|
||||||
:title="dialogType === 'add' ? $t('page.form.dialogTitleAdd') : $t('page.form.dialogTitleEdit')"
|
:title="$t('page.form.dialogTitleEdit')"
|
||||||
width="600px"
|
width="600px"
|
||||||
align-center
|
align-center
|
||||||
:close-on-click-modal="false"
|
:close-on-click-modal="false"
|
||||||
@close="handleClose"
|
@close="handleClose"
|
||||||
>
|
>
|
||||||
<el-form ref="formRef" :model="formData" :rules="rules" label-width="120px">
|
<el-form ref="formRef" :model="formData" label-width="120px">
|
||||||
<el-form-item :label="$t('page.form.player')" prop="player_id">
|
<el-form-item :label="$t('page.form.player')" prop="player_id">
|
||||||
<el-select
|
<el-select
|
||||||
v-model="formData.player_id"
|
v-model="formData.player_id"
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
clearable
|
clearable
|
||||||
filterable
|
filterable
|
||||||
style="width: 100%"
|
style="width: 100%"
|
||||||
:disabled="dialogType === 'edit'"
|
disabled
|
||||||
>
|
>
|
||||||
<el-option
|
<el-option
|
||||||
v-for="item in playerOptions"
|
v-for="item in playerOptions"
|
||||||
@@ -32,12 +32,12 @@
|
|||||||
clearable
|
clearable
|
||||||
filterable
|
filterable
|
||||||
style="width: 100%"
|
style="width: 100%"
|
||||||
:disabled="dialogType === 'edit'"
|
disabled
|
||||||
>
|
>
|
||||||
<el-option
|
<el-option
|
||||||
v-for="item in lotteryConfigOptions"
|
v-for="item in lotteryConfigOptions"
|
||||||
:key="item.id"
|
:key="item.id"
|
||||||
:label="item.name"
|
:label="lotteryPoolOptionLabel(item)"
|
||||||
:value="item.id"
|
:value="item.id"
|
||||||
/>
|
/>
|
||||||
</el-select>
|
</el-select>
|
||||||
@@ -48,7 +48,7 @@
|
|||||||
:placeholder="$t('form.placeholderSelect')"
|
:placeholder="$t('form.placeholderSelect')"
|
||||||
clearable
|
clearable
|
||||||
style="width: 100%"
|
style="width: 100%"
|
||||||
:disabled="dialogType === 'edit'"
|
disabled
|
||||||
>
|
>
|
||||||
<el-option :label="$t('page.form.paid')" :value="0" />
|
<el-option :label="$t('page.form.paid')" :value="0" />
|
||||||
<el-option :label="$t('page.form.free')" :value="1" />
|
<el-option :label="$t('page.form.free')" :value="1" />
|
||||||
@@ -60,7 +60,7 @@
|
|||||||
:placeholder="$t('form.placeholderSelect')"
|
:placeholder="$t('form.placeholderSelect')"
|
||||||
clearable
|
clearable
|
||||||
style="width: 100%"
|
style="width: 100%"
|
||||||
:disabled="dialogType === 'edit'"
|
disabled
|
||||||
>
|
>
|
||||||
<el-option :label="$t('page.form.noBigWin')" :value="0" />
|
<el-option :label="$t('page.form.noBigWin')" :value="0" />
|
||||||
<el-option :label="$t('page.form.bigWin')" :value="1" />
|
<el-option :label="$t('page.form.bigWin')" :value="1" />
|
||||||
@@ -72,7 +72,7 @@
|
|||||||
:placeholder="$t('page.form.placeholderWinCoin')"
|
:placeholder="$t('page.form.placeholderWinCoin')"
|
||||||
:precision="0"
|
:precision="0"
|
||||||
style="width: 100%"
|
style="width: 100%"
|
||||||
:disabled="dialogType === 'edit'"
|
disabled
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item :label="$t('page.form.superWinCoin')" prop="super_win_coin">
|
<el-form-item :label="$t('page.form.superWinCoin')" prop="super_win_coin">
|
||||||
@@ -82,7 +82,7 @@
|
|||||||
:precision="0"
|
:precision="0"
|
||||||
:min="0"
|
:min="0"
|
||||||
style="width: 100%"
|
style="width: 100%"
|
||||||
:disabled="dialogType === 'edit'"
|
disabled
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item :label="$t('page.form.rewardWinCoin')" prop="reward_win_coin">
|
<el-form-item :label="$t('page.form.rewardWinCoin')" prop="reward_win_coin">
|
||||||
@@ -92,7 +92,7 @@
|
|||||||
:precision="0"
|
:precision="0"
|
||||||
:min="0"
|
:min="0"
|
||||||
style="width: 100%"
|
style="width: 100%"
|
||||||
:disabled="dialogType === 'edit'"
|
disabled
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item :label="$t('page.form.direction')" prop="direction">
|
<el-form-item :label="$t('page.form.direction')" prop="direction">
|
||||||
@@ -101,7 +101,7 @@
|
|||||||
:placeholder="$t('page.form.placeholderDirection')"
|
:placeholder="$t('page.form.placeholderDirection')"
|
||||||
clearable
|
clearable
|
||||||
style="width: 100%"
|
style="width: 100%"
|
||||||
:disabled="dialogType === 'edit'"
|
disabled
|
||||||
>
|
>
|
||||||
<el-option :label="$t('page.form.clockwise')" :value="0" />
|
<el-option :label="$t('page.form.clockwise')" :value="0" />
|
||||||
<el-option :label="$t('page.form.anticlockwise')" :value="1" />
|
<el-option :label="$t('page.form.anticlockwise')" :value="1" />
|
||||||
@@ -113,7 +113,7 @@
|
|||||||
:placeholder="$t('page.form.placeholderStartIndex')"
|
:placeholder="$t('page.form.placeholderStartIndex')"
|
||||||
:min="0"
|
:min="0"
|
||||||
style="width: 100%"
|
style="width: 100%"
|
||||||
:disabled="dialogType === 'edit'"
|
disabled
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item :label="$t('page.form.targetIndex')" prop="target_index">
|
<el-form-item :label="$t('page.form.targetIndex')" prop="target_index">
|
||||||
@@ -122,7 +122,7 @@
|
|||||||
:placeholder="$t('page.form.placeholderTargetIndex')"
|
:placeholder="$t('page.form.placeholderTargetIndex')"
|
||||||
:min="0"
|
:min="0"
|
||||||
style="width: 100%"
|
style="width: 100%"
|
||||||
:disabled="dialogType === 'edit'"
|
disabled
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item :label="$t('page.form.rollArray')" prop="rollArrayItems">
|
<el-form-item :label="$t('page.form.rollArray')" prop="rollArrayItems">
|
||||||
@@ -137,7 +137,7 @@
|
|||||||
controls-position="right"
|
controls-position="right"
|
||||||
placeholder=""
|
placeholder=""
|
||||||
class="roll-array-input"
|
class="roll-array-input"
|
||||||
:disabled="dialogType === 'edit'"
|
disabled
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="roll-array-hint">{{ $t('page.form.rollArrayHint') }}</div>
|
<div class="roll-array-hint">{{ $t('page.form.rollArrayHint') }}</div>
|
||||||
@@ -150,7 +150,7 @@
|
|||||||
:max="30"
|
:max="30"
|
||||||
:precision="0"
|
:precision="0"
|
||||||
style="width: 100%"
|
style="width: 100%"
|
||||||
:disabled="dialogType === 'edit'"
|
disabled
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item :label="$t('page.form.rewardTier')" prop="reward_tier">
|
<el-form-item :label="$t('page.form.rewardTier')" prop="reward_tier">
|
||||||
@@ -160,7 +160,7 @@
|
|||||||
clearable
|
clearable
|
||||||
filterable
|
filterable
|
||||||
style="width: 100%"
|
style="width: 100%"
|
||||||
:disabled="true"
|
disabled
|
||||||
>
|
>
|
||||||
<el-option label="T1" value="T1" />
|
<el-option label="T1" value="T1" />
|
||||||
<el-option label="T2" value="T2" />
|
<el-option label="T2" value="T2" />
|
||||||
@@ -172,20 +172,19 @@
|
|||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<el-button @click="handleClose">{{ dialogType === 'edit' ? $t('form.close') : $t('common.cancel') }}</el-button>
|
<el-button @click="handleClose">{{ $t('form.close') }}</el-button>
|
||||||
<el-button v-if="dialogType === 'add'" type="primary" @click="handleSubmit">{{ $t('table.form.submit') }}</el-button>
|
|
||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import api from '../../../api/play_record/index'
|
import api from '../../../api/play_record/index'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { getChannelDeptRequestParams } from '@/composables/useChannelDeptScope'
|
||||||
import { getChannelDeptRequestParams, withChannelDeptParams } from '@/composables/useChannelDeptScope'
|
import {
|
||||||
import { ElMessage } from 'element-plus'
|
lotteryPoolOptionLabel,
|
||||||
import type { FormInstance, FormRules } from 'element-plus'
|
type LotteryPoolOption
|
||||||
|
} from '@/views/plugin/dice/utils/lotteryPoolDisplay'
|
||||||
const { t } = useI18n()
|
import type { FormInstance } from 'element-plus'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
modelValue: boolean
|
modelValue: boolean
|
||||||
@@ -200,7 +199,7 @@
|
|||||||
|
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
modelValue: false,
|
modelValue: false,
|
||||||
dialogType: 'add',
|
dialogType: 'edit',
|
||||||
data: undefined
|
data: undefined
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -213,34 +212,8 @@
|
|||||||
set: (value) => emit('update:modelValue', value)
|
set: (value) => emit('update:modelValue', value)
|
||||||
})
|
})
|
||||||
|
|
||||||
const rules = computed<FormRules>(() => ({
|
|
||||||
player_id: [{ required: true, message: t('page.form.rulePlayerRequired'), trigger: 'change' }],
|
|
||||||
lottery_config_id: [{ required: true, message: t('page.form.ruleLotteryConfigRequired'), trigger: 'change' }],
|
|
||||||
lottery_type: [{ required: true, message: t('page.form.ruleLotteryTypeRequired'), trigger: 'change' }],
|
|
||||||
is_win: [{ required: true, message: t('page.form.ruleIsWinRequired'), trigger: 'change' }],
|
|
||||||
win_coin: [{ required: true, message: t('page.form.ruleWinCoinRequired'), trigger: 'blur' }],
|
|
||||||
rollArrayItems: [
|
|
||||||
{
|
|
||||||
validator: (_rule: any, value: (number | null)[], callback: (e?: Error) => void) => {
|
|
||||||
if (!value || value.length !== 5) {
|
|
||||||
callback(new Error(t('page.form.ruleRollArrayLength')))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const ok = value.every((n) => n != null && n >= 1 && n <= 6)
|
|
||||||
if (!ok) {
|
|
||||||
callback(new Error(t('page.form.ruleRollArrayValues')))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
callback()
|
|
||||||
},
|
|
||||||
trigger: 'change'
|
|
||||||
}
|
|
||||||
],
|
|
||||||
reward_tier: [{ required: true, message: t('page.form.ruleRewardTierRequired'), trigger: 'change' }]
|
|
||||||
}))
|
|
||||||
|
|
||||||
const playerOptions = ref<Array<{ id: number; username: string }>>([])
|
const playerOptions = ref<Array<{ id: number; username: string }>>([])
|
||||||
const lotteryConfigOptions = ref<Array<{ id: number; name: string }>>([])
|
const lotteryConfigOptions = ref<LotteryPoolOption[]>([])
|
||||||
|
|
||||||
const initialFormData = {
|
const initialFormData = {
|
||||||
id: null as number | null,
|
id: null as number | null,
|
||||||
@@ -376,46 +349,6 @@
|
|||||||
visible.value = false
|
visible.value = false
|
||||||
formRef.value?.resetFields()
|
formRef.value?.resetFields()
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
|
||||||
if (!formRef.value) return
|
|
||||||
try {
|
|
||||||
await formRef.value.validate()
|
|
||||||
const payload = { ...formData } as Record<string, unknown>
|
|
||||||
// 将 5 个输入值拼成 [1,2,3,4,5] 格式,确保每项为 1~6 的整数
|
|
||||||
const items = formData.rollArrayItems
|
|
||||||
const rollArray = items.map((n) => {
|
|
||||||
const v = n != null ? Number(n) : 1
|
|
||||||
return Math.min(6, Math.max(1, Number.isNaN(v) ? 1 : Math.floor(v)))
|
|
||||||
})
|
|
||||||
payload.roll_array = rollArray
|
|
||||||
payload.roll_number = formData.roll_number ?? rollArray.reduce((s, n) => s + n, 0)
|
|
||||||
delete payload.rollArrayItems
|
|
||||||
if (props.dialogType === 'add') {
|
|
||||||
delete payload.id
|
|
||||||
await api.save(withChannelDeptParams(payload))
|
|
||||||
ElMessage.success(t('page.form.addSuccess'))
|
|
||||||
} else {
|
|
||||||
await api.update(withChannelDeptParams(payload))
|
|
||||||
ElMessage.success(t('page.form.editSuccess'))
|
|
||||||
}
|
|
||||||
emit('success')
|
|
||||||
handleClose()
|
|
||||||
} catch (error: any) {
|
|
||||||
let msg = t('page.form.validateFailed')
|
|
||||||
if (error?.message) {
|
|
||||||
msg = error.message
|
|
||||||
} else if (typeof error === 'string') {
|
|
||||||
msg = error
|
|
||||||
} else if (error && typeof error === 'object') {
|
|
||||||
const first = Object.values(error).find((v: any) => v?.[0]?.message)
|
|
||||||
if (first && Array.isArray(first)) {
|
|
||||||
msg = (first[0] as any).message || msg
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ElMessage.warning(msg)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|||||||
@@ -105,6 +105,19 @@
|
|||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
|
<el-col v-bind="setSpan(8)">
|
||||||
|
<el-form-item :label="$t('page.search.createTime')" prop="create_time">
|
||||||
|
<el-date-picker
|
||||||
|
v-model="formData.create_time"
|
||||||
|
type="datetimerange"
|
||||||
|
:range-separator="$t('table.searchBar.rangeSeparator')"
|
||||||
|
:start-placeholder="$t('table.searchBar.startTime')"
|
||||||
|
:end-placeholder="$t('table.searchBar.endTime')"
|
||||||
|
value-format="YYYY-MM-DD HH:mm:ss"
|
||||||
|
style="width: 100%"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
</sa-search-bar>
|
</sa-search-bar>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -155,10 +155,12 @@
|
|||||||
import TableSearch from './modules/table-search.vue'
|
import TableSearch from './modules/table-search.vue'
|
||||||
import EditDialog from './modules/edit-dialog.vue'
|
import EditDialog from './modules/edit-dialog.vue'
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
|
import { lotteryPoolRowLabel } from '@/views/plugin/dice/utils/lotteryPoolDisplay'
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
|
|
||||||
// 搜索表单(与 play_record 对齐:方向、赢取平台币范围、是否中大奖、中奖档位、点数和)
|
// 搜索表单(与 play_record 对齐:方向、赢取平台币范围、是否中大奖、中奖档位、点数和)
|
||||||
const searchForm = ref<Record<string, unknown>>({
|
const searchForm = ref<Record<string, unknown>>({
|
||||||
|
lottery_config_id: undefined,
|
||||||
reward_config_record_id: undefined,
|
reward_config_record_id: undefined,
|
||||||
lottery_type: undefined,
|
lottery_type: undefined,
|
||||||
direction: undefined,
|
direction: undefined,
|
||||||
@@ -180,8 +182,7 @@
|
|||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
|
|
||||||
const lotteryConfigNameFormatter = (row: Record<string, any>) =>
|
const lotteryConfigNameFormatter = (row: Record<string, any>) => lotteryPoolRowLabel(row)
|
||||||
row?.diceLotteryPoolConfig?.name ?? row?.lottery_config_id ?? '-'
|
|
||||||
const rewardTierFormatter = (row: Record<string, any>) => row?.reward_tier ?? '-'
|
const rewardTierFormatter = (row: Record<string, any>) => row?.reward_tier ?? '-'
|
||||||
|
|
||||||
/** 摇取点数格式化为 1,3,4,5,6 */
|
/** 摇取点数格式化为 1,3,4,5,6 */
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
<template>
|
<template>
|
||||||
<el-dialog
|
<el-dialog
|
||||||
v-model="visible"
|
v-model="visible"
|
||||||
:title="dialogType === 'add' ? $t('page.form.titleAdd') : $t('page.form.titleEdit')"
|
:title="$t('page.form.titleEdit')"
|
||||||
width="600px"
|
width="600px"
|
||||||
align-center
|
align-center
|
||||||
:close-on-click-modal="false"
|
:close-on-click-modal="false"
|
||||||
@close="handleClose"
|
@close="handleClose"
|
||||||
>
|
>
|
||||||
<el-form ref="formRef" :model="formData" :rules="rules" label-width="120px">
|
<el-form ref="formRef" :model="formData" label-width="120px">
|
||||||
<el-form-item :label="$t('page.form.labelLotteryConfigId')" prop="lottery_config_id">
|
<el-form-item :label="$t('page.form.labelLotteryConfigId')" prop="lottery_config_id">
|
||||||
<el-input v-model="formData.lottery_config_id" :placeholder="$t('page.form.placeholderLotteryConfigId')" />
|
<el-input v-model="formData.lottery_config_id" :placeholder="$t('page.form.placeholderLotteryConfigId')" disabled />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item :label="$t('page.form.drawType')" prop="lottery_type">
|
<el-form-item :label="$t('page.form.drawType')" prop="lottery_type">
|
||||||
<el-select
|
<el-select
|
||||||
@@ -17,13 +17,20 @@
|
|||||||
:placeholder="$t('form.placeholderSelect')"
|
:placeholder="$t('form.placeholderSelect')"
|
||||||
clearable
|
clearable
|
||||||
style="width: 100%"
|
style="width: 100%"
|
||||||
|
disabled
|
||||||
>
|
>
|
||||||
<el-option :label="$t('page.search.paid')" :value="0" />
|
<el-option :label="$t('page.search.paid')" :value="0" />
|
||||||
<el-option :label="$t('page.search.free')" :value="1" />
|
<el-option :label="$t('page.search.free')" :value="1" />
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item :label="$t('page.search.direction')" prop="direction">
|
<el-form-item :label="$t('page.search.direction')" prop="direction">
|
||||||
<el-select v-model="formData.direction" :placeholder="$t('form.placeholderSelect')" clearable style="width: 100%">
|
<el-select
|
||||||
|
v-model="formData.direction"
|
||||||
|
:placeholder="$t('form.placeholderSelect')"
|
||||||
|
clearable
|
||||||
|
style="width: 100%"
|
||||||
|
disabled
|
||||||
|
>
|
||||||
<el-option :label="$t('page.search.clockwise')" :value="0" />
|
<el-option :label="$t('page.search.clockwise')" :value="0" />
|
||||||
<el-option :label="$t('page.search.anticlockwise')" :value="1" />
|
<el-option :label="$t('page.search.anticlockwise')" :value="1" />
|
||||||
</el-select>
|
</el-select>
|
||||||
@@ -35,6 +42,7 @@
|
|||||||
:precision="0"
|
:precision="0"
|
||||||
controls-position="right"
|
controls-position="right"
|
||||||
style="width: 100%"
|
style="width: 100%"
|
||||||
|
disabled
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item :label="$t('page.search.paidAmount')" prop="paid_amount">
|
<el-form-item :label="$t('page.search.paidAmount')" prop="paid_amount">
|
||||||
@@ -44,10 +52,11 @@
|
|||||||
:precision="0"
|
:precision="0"
|
||||||
controls-position="right"
|
controls-position="right"
|
||||||
style="width: 100%"
|
style="width: 100%"
|
||||||
|
disabled
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item :label="$t('page.search.isBigWin')" prop="is_win">
|
<el-form-item :label="$t('page.search.isBigWin')" prop="is_win">
|
||||||
<el-select v-model="formData.is_win" :placeholder="$t('form.placeholderSelect')" clearable style="width: 100%">
|
<el-select v-model="formData.is_win" :placeholder="$t('form.placeholderSelect')" clearable style="width: 100%" disabled>
|
||||||
<el-option :label="$t('page.search.noBigWin')" :value="0" />
|
<el-option :label="$t('page.search.noBigWin')" :value="0" />
|
||||||
<el-option :label="$t('page.search.bigWin')" :value="1" />
|
<el-option :label="$t('page.search.bigWin')" :value="1" />
|
||||||
</el-select>
|
</el-select>
|
||||||
@@ -59,6 +68,7 @@
|
|||||||
:precision="0"
|
:precision="0"
|
||||||
controls-position="right"
|
controls-position="right"
|
||||||
style="width: 100%"
|
style="width: 100%"
|
||||||
|
disabled
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item :label="$t('page.search.rewardTier')" prop="reward_tier">
|
<el-form-item :label="$t('page.search.rewardTier')" prop="reward_tier">
|
||||||
@@ -67,6 +77,7 @@
|
|||||||
:placeholder="$t('page.form.placeholderRewardTier')"
|
:placeholder="$t('page.form.placeholderRewardTier')"
|
||||||
clearable
|
clearable
|
||||||
style="width: 100%"
|
style="width: 100%"
|
||||||
|
disabled
|
||||||
>
|
>
|
||||||
<el-option label="T1" value="T1" />
|
<el-option label="T1" value="T1" />
|
||||||
<el-option label="T2" value="T2" />
|
<el-option label="T2" value="T2" />
|
||||||
@@ -77,19 +88,19 @@
|
|||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item :label="$t('page.table.startIndex')" prop="start_index">
|
<el-form-item :label="$t('page.table.startIndex')" prop="start_index">
|
||||||
<el-input v-model="formData.start_index" :placeholder="$t('page.form.placeholderStartIndex')" />
|
<el-input v-model="formData.start_index" :placeholder="$t('page.form.placeholderStartIndex')" disabled />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item :label="$t('page.form.labelTargetIndex')" prop="target_index">
|
<el-form-item :label="$t('page.form.labelTargetIndex')" prop="target_index">
|
||||||
<el-input v-model="formData.target_index" :placeholder="$t('page.form.placeholderTargetIndex')" />
|
<el-input v-model="formData.target_index" :placeholder="$t('page.form.placeholderTargetIndex')" disabled />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item :label="$t('page.search.rollNumber')" prop="roll_number">
|
<el-form-item :label="$t('page.search.rollNumber')" prop="roll_number">
|
||||||
<el-input v-model="formData.roll_number" :placeholder="$t('page.form.placeholderRollNumber')" />
|
<el-input v-model="formData.roll_number" :placeholder="$t('page.form.placeholderRollNumber')" disabled />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item :label="$t('page.form.labelRollArray')" prop="roll_array">
|
<el-form-item :label="$t('page.form.labelRollArray')" prop="roll_array">
|
||||||
<el-input v-model="formData.roll_array" :placeholder="$t('page.form.placeholderRollArray')" />
|
<el-input v-model="formData.roll_array" :placeholder="$t('page.form.placeholderRollArray')" disabled />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item :label="$t('page.form.labelStatus')" prop="status">
|
<el-form-item :label="$t('page.form.labelStatus')" prop="status">
|
||||||
<sa-radio v-model="formData.status" dict="data_status" />
|
<sa-radio v-model="formData.status" dict="data_status" disabled />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item :label="$t('page.table.superWinCoin')" prop="super_win_coin">
|
<el-form-item :label="$t('page.table.superWinCoin')" prop="super_win_coin">
|
||||||
<el-input-number
|
<el-input-number
|
||||||
@@ -98,6 +109,7 @@
|
|||||||
:precision="0"
|
:precision="0"
|
||||||
controls-position="right"
|
controls-position="right"
|
||||||
style="width: 100%"
|
style="width: 100%"
|
||||||
|
disabled
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item :label="$t('page.table.rewardWinCoin')" prop="reward_win_coin">
|
<el-form-item :label="$t('page.table.rewardWinCoin')" prop="reward_win_coin">
|
||||||
@@ -107,25 +119,21 @@
|
|||||||
:precision="0"
|
:precision="0"
|
||||||
controls-position="right"
|
controls-position="right"
|
||||||
style="width: 100%"
|
style="width: 100%"
|
||||||
|
disabled
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item :label="$t('page.form.labelAdminId')" prop="admin_id">
|
<el-form-item :label="$t('page.form.labelAdminId')" prop="admin_id">
|
||||||
<el-input v-model="formData.admin_id" :placeholder="$t('page.form.placeholderAdminId')" />
|
<el-input v-model="formData.admin_id" :placeholder="$t('page.form.placeholderAdminId')" disabled />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<el-button @click="handleClose">{{ $t('common.cancel') }}</el-button>
|
<el-button @click="handleClose">{{ $t('form.close') }}</el-button>
|
||||||
<el-button type="primary" @click="handleSubmit">{{ $t('table.form.submit') }}</el-button>
|
|
||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import api from '../../../api/play_record_test/index'
|
import type { FormInstance } from 'element-plus'
|
||||||
import { ElMessage } from 'element-plus'
|
|
||||||
import type { FormInstance, FormRules } from 'element-plus'
|
|
||||||
import { useI18n } from 'vue-i18n'
|
|
||||||
import { withChannelDeptParams } from '@/composables/useChannelDeptScope'
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
modelValue: boolean
|
modelValue: boolean
|
||||||
@@ -140,12 +148,11 @@
|
|||||||
|
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
modelValue: false,
|
modelValue: false,
|
||||||
dialogType: 'add',
|
dialogType: 'edit',
|
||||||
data: undefined
|
data: undefined
|
||||||
})
|
})
|
||||||
|
|
||||||
const emit = defineEmits<Emits>()
|
const emit = defineEmits<Emits>()
|
||||||
const { t } = useI18n()
|
|
||||||
|
|
||||||
const formRef = ref<FormInstance>()
|
const formRef = ref<FormInstance>()
|
||||||
|
|
||||||
@@ -157,18 +164,6 @@
|
|||||||
set: (value) => emit('update:modelValue', value)
|
set: (value) => emit('update:modelValue', value)
|
||||||
})
|
})
|
||||||
|
|
||||||
/**
|
|
||||||
* 表单验证规则
|
|
||||||
*/
|
|
||||||
const rules = computed<FormRules>(() => ({
|
|
||||||
lottery_config_id: [{ required: true, message: t('page.form.ruleLotteryConfigIdRequired'), trigger: 'blur' }],
|
|
||||||
lottery_type: [{ required: true, message: t('page.form.ruleDrawTypeRequired'), trigger: 'blur' }],
|
|
||||||
is_win: [{ required: true, message: t('page.form.ruleIsBigWinRequired'), trigger: 'blur' }],
|
|
||||||
direction: [{ required: true, message: t('page.form.ruleDirectionRequired'), trigger: 'blur' }],
|
|
||||||
reward_tier: [{ required: true, message: t('page.form.ruleRewardTierRequired'), trigger: 'blur' }],
|
|
||||||
status: [{ required: true, message: t('page.form.ruleStatusRequired'), trigger: 'blur' }]
|
|
||||||
}))
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 初始数据
|
* 初始数据
|
||||||
*/
|
*/
|
||||||
@@ -198,7 +193,7 @@
|
|||||||
const formData = reactive({ ...initialFormData })
|
const formData = reactive({ ...initialFormData })
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 监听弹窗打开,初始化表单数据
|
* 监听弹窗打开,初始化表单数据(仅查看)
|
||||||
*/
|
*/
|
||||||
watch(
|
watch(
|
||||||
() => props.modelValue,
|
() => props.modelValue,
|
||||||
@@ -213,9 +208,7 @@
|
|||||||
* 初始化页面数据
|
* 初始化页面数据
|
||||||
*/
|
*/
|
||||||
const initPage = async () => {
|
const initPage = async () => {
|
||||||
// 先重置为初始值
|
|
||||||
Object.assign(formData, initialFormData)
|
Object.assign(formData, initialFormData)
|
||||||
// 如果有数据,则填充数据
|
|
||||||
if (props.data) {
|
if (props.data) {
|
||||||
await nextTick()
|
await nextTick()
|
||||||
initForm()
|
initForm()
|
||||||
@@ -223,7 +216,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 初始化表单数据
|
* 回填表单数据
|
||||||
*/
|
*/
|
||||||
function normalizePlatformCoin(val: unknown): number {
|
function normalizePlatformCoin(val: unknown): number {
|
||||||
if (val === '' || val === null || val === undefined) return 0
|
if (val === '' || val === null || val === undefined) return 0
|
||||||
@@ -245,34 +238,11 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 关闭弹窗并重置表单
|
* 关闭弹窗
|
||||||
*/
|
*/
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
visible.value = false
|
visible.value = false
|
||||||
formRef.value?.resetFields()
|
formRef.value?.resetFields()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 提交表单
|
|
||||||
*/
|
|
||||||
const handleSubmit = async () => {
|
|
||||||
if (!formRef.value) return
|
|
||||||
try {
|
|
||||||
await formRef.value.validate()
|
|
||||||
const payload = { ...formData }
|
|
||||||
if (props.dialogType === 'add') {
|
|
||||||
await api.save(withChannelDeptParams(payload))
|
|
||||||
ElMessage.success(t('page.form.addSuccess'))
|
|
||||||
} else {
|
|
||||||
await api.update(withChannelDeptParams(payload))
|
|
||||||
ElMessage.success(t('page.form.editSuccess'))
|
|
||||||
}
|
|
||||||
emit('success')
|
|
||||||
handleClose()
|
|
||||||
} catch (error) {
|
|
||||||
console.log('表单验证失败:', error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -8,6 +8,29 @@
|
|||||||
@search="handleSearch"
|
@search="handleSearch"
|
||||||
@expand="handleExpand"
|
@expand="handleExpand"
|
||||||
>
|
>
|
||||||
|
<el-col v-bind="setSpan(6)">
|
||||||
|
<el-form-item :label="$t('page.search.lotteryPoolConfig')" prop="lottery_config_id">
|
||||||
|
<el-select
|
||||||
|
v-model="formData.lottery_config_id"
|
||||||
|
:placeholder="$t('page.search.placeholderLotteryPool')"
|
||||||
|
clearable
|
||||||
|
filterable
|
||||||
|
remote
|
||||||
|
reserve-keyword
|
||||||
|
:loading="lotteryPoolLoading"
|
||||||
|
:remote-method="filterLotteryPoolOptions"
|
||||||
|
style="width: 100%"
|
||||||
|
@visible-change="onLotteryPoolDropdownVisible"
|
||||||
|
>
|
||||||
|
<el-option
|
||||||
|
v-for="item in lotteryPoolOptions"
|
||||||
|
:key="item.id"
|
||||||
|
:label="lotteryPoolOptionLabel(item)"
|
||||||
|
:value="item.id"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
<el-col v-bind="setSpan(6)">
|
<el-col v-bind="setSpan(6)">
|
||||||
<el-form-item :label="$t('page.search.rewardConfigRecordId')" prop="reward_config_record_id">
|
<el-form-item :label="$t('page.search.rewardConfigRecordId')" prop="reward_config_record_id">
|
||||||
<el-input-number
|
<el-input-number
|
||||||
@@ -120,6 +143,17 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import lotteryPoolApi from '../../../api/lottery_pool_config/index'
|
||||||
|
import {
|
||||||
|
getChannelDeptRequestParams,
|
||||||
|
useInjectedChannelDept
|
||||||
|
} from '@/composables/useChannelDeptScope'
|
||||||
|
import {
|
||||||
|
filterLotteryPoolOptionsByQuery,
|
||||||
|
lotteryPoolOptionLabel,
|
||||||
|
type LotteryPoolOption
|
||||||
|
} from '@/views/plugin/dice/utils/lotteryPoolDisplay'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
modelValue: Record<string, any>
|
modelValue: Record<string, any>
|
||||||
}
|
}
|
||||||
@@ -131,6 +165,57 @@
|
|||||||
const props = defineProps<Props>()
|
const props = defineProps<Props>()
|
||||||
const emit = defineEmits<Emits>()
|
const emit = defineEmits<Emits>()
|
||||||
const isExpanded = ref<boolean>(false)
|
const isExpanded = ref<boolean>(false)
|
||||||
|
const channelScope = useInjectedChannelDept()
|
||||||
|
|
||||||
|
const lotteryPoolAllOptions = ref<LotteryPoolOption[]>([])
|
||||||
|
const lotteryPoolOptions = ref<LotteryPoolOption[]>([])
|
||||||
|
const lotteryPoolLoading = ref(false)
|
||||||
|
|
||||||
|
function resolveDeptParams(): Record<string, unknown> {
|
||||||
|
const extra = getChannelDeptRequestParams()
|
||||||
|
if (extra.dept_id !== undefined) {
|
||||||
|
return extra
|
||||||
|
}
|
||||||
|
if (channelScope) {
|
||||||
|
return { dept_id: channelScope.selectedDeptId.value }
|
||||||
|
}
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadLotteryPoolOptions() {
|
||||||
|
lotteryPoolLoading.value = true
|
||||||
|
try {
|
||||||
|
const list = await lotteryPoolApi.getOptions(resolveDeptParams())
|
||||||
|
lotteryPoolAllOptions.value = list
|
||||||
|
lotteryPoolOptions.value = list
|
||||||
|
} catch {
|
||||||
|
lotteryPoolAllOptions.value = []
|
||||||
|
lotteryPoolOptions.value = []
|
||||||
|
} finally {
|
||||||
|
lotteryPoolLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function filterLotteryPoolOptions(query: string) {
|
||||||
|
lotteryPoolOptions.value = filterLotteryPoolOptionsByQuery(lotteryPoolAllOptions.value, query)
|
||||||
|
}
|
||||||
|
|
||||||
|
function onLotteryPoolDropdownVisible(visible: boolean) {
|
||||||
|
if (visible && lotteryPoolAllOptions.value.length === 0) {
|
||||||
|
void loadLotteryPoolOptions()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
void loadLotteryPoolOptions()
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => channelScope?.selectedDeptId.value,
|
||||||
|
() => {
|
||||||
|
void loadLotteryPoolOptions()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
const searchBarRef = ref()
|
const searchBarRef = ref()
|
||||||
const formData = computed({
|
const formData = computed({
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="art-full-height">
|
<div class="art-full-height">
|
||||||
|
<QuickDateRangeBar v-model="activeDatePreset" @select="handleQuickDateSelect" />
|
||||||
<!-- 搜索条件 -->
|
<!-- 搜索条件 -->
|
||||||
<TableSearch v-model="searchForm" @search="handleSearch" @reset="resetSearchParams" />
|
<TableSearch v-model="searchForm" @search="handleSearch" @reset="handleResetSearch" />
|
||||||
|
|
||||||
<ElCard class="art-table-card" shadow="never">
|
<ElCard class="art-table-card" shadow="never">
|
||||||
<!-- 表格操作 -->
|
<!-- 表格操作 -->
|
||||||
@@ -108,6 +109,7 @@
|
|||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
import { useClipboard } from '@vueuse/core'
|
import { useClipboard } from '@vueuse/core'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import { useRoute } from 'vue-router'
|
||||||
import { useTable } from '@/hooks/core/useTable'
|
import { useTable } from '@/hooks/core/useTable'
|
||||||
import { useSaiAdmin } from '@/composables/useSaiAdmin'
|
import { useSaiAdmin } from '@/composables/useSaiAdmin'
|
||||||
import api from '../../api/player/index'
|
import api from '../../api/player/index'
|
||||||
@@ -115,9 +117,19 @@
|
|||||||
import TableSearch from './modules/table-search.vue'
|
import TableSearch from './modules/table-search.vue'
|
||||||
import EditDialog from './modules/edit-dialog.vue'
|
import EditDialog from './modules/edit-dialog.vue'
|
||||||
import WalletOperateDialog from './modules/WalletOperateDialog.vue'
|
import WalletOperateDialog from './modules/WalletOperateDialog.vue'
|
||||||
|
import { lotteryPoolRowLabel } from '@/views/plugin/dice/utils/lotteryPoolDisplay'
|
||||||
|
import QuickDateRangeBar from '@/views/plugin/dice/components/QuickDateRangeBar.vue'
|
||||||
|
import {
|
||||||
|
detectPresetFromRange,
|
||||||
|
type DatePresetKey
|
||||||
|
} from '@/views/plugin/dice/utils/dateRangePresets'
|
||||||
|
import { getRecordRouteInit, useRecordRouteSync } from '@/views/plugin/dice/composables/useRecordRouteQuery'
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
const { copy } = useClipboard()
|
const { copy } = useClipboard()
|
||||||
|
const route = useRoute()
|
||||||
|
const routeInit = getRecordRouteInit(route.query)
|
||||||
|
const activeDatePreset = ref<DatePresetKey | null>(routeInit.activePreset)
|
||||||
|
|
||||||
// 搜索表单
|
// 搜索表单
|
||||||
const searchForm = ref({
|
const searchForm = ref({
|
||||||
@@ -126,25 +138,77 @@
|
|||||||
phone: undefined,
|
phone: undefined,
|
||||||
status: undefined,
|
status: undefined,
|
||||||
coin: undefined,
|
coin: undefined,
|
||||||
lottery_config_id: undefined
|
lottery_config_id: undefined,
|
||||||
|
create_time: routeInit.create_time
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const PLAYER_SEARCH_KEYS = [
|
||||||
|
'username',
|
||||||
|
'name',
|
||||||
|
'phone',
|
||||||
|
'status',
|
||||||
|
'coin',
|
||||||
|
'lottery_config_id',
|
||||||
|
'create_time_min',
|
||||||
|
'create_time_max'
|
||||||
|
] as const
|
||||||
|
|
||||||
|
const applySearchParams = (params: Record<string, unknown>) => {
|
||||||
|
const p = { ...params }
|
||||||
|
if (Array.isArray(p.create_time) && p.create_time.length === 2) {
|
||||||
|
p.create_time_min = p.create_time[0]
|
||||||
|
p.create_time_max = p.create_time[1]
|
||||||
|
}
|
||||||
|
delete p.create_time
|
||||||
|
const paramsRecord = searchParams as Record<string, unknown>
|
||||||
|
PLAYER_SEARCH_KEYS.forEach((key) => {
|
||||||
|
delete paramsRecord[key]
|
||||||
|
})
|
||||||
|
Object.assign(searchParams, p)
|
||||||
|
}
|
||||||
|
|
||||||
// 搜索
|
// 搜索
|
||||||
const handleSearch = (params: Record<string, any>) => {
|
const handleSearch = (params: Record<string, any>) => {
|
||||||
Object.assign(searchParams, params)
|
applySearchParams(params)
|
||||||
getData()
|
getData()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleQuickDateSelect = (range: [string, string], preset: DatePresetKey) => {
|
||||||
|
searchForm.value.create_time = range
|
||||||
|
activeDatePreset.value = preset
|
||||||
|
handleSearch({ ...searchForm.value })
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleResetSearch = () => {
|
||||||
|
activeDatePreset.value = null
|
||||||
|
searchForm.value.create_time = undefined
|
||||||
|
resetSearchParams()
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => searchForm.value.create_time,
|
||||||
|
(range) => {
|
||||||
|
if (Array.isArray(range) && range.length === 2) {
|
||||||
|
activeDatePreset.value = detectPresetFromRange([range[0], range[1]])
|
||||||
|
} else {
|
||||||
|
activeDatePreset.value = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
// 权重列显示为百分比
|
// 权重列显示为百分比
|
||||||
const weightFormatter = (prop: string) => (row: any) => {
|
const weightFormatter = (prop: string) => (row: any) => {
|
||||||
const cellValue = row[prop]
|
const cellValue = row[prop]
|
||||||
return cellValue != null && cellValue !== '' ? `${cellValue}%` : '-'
|
return cellValue != null && cellValue !== '' ? `${cellValue}%` : '-'
|
||||||
}
|
}
|
||||||
|
|
||||||
// 根据 lottery_config_id 显示彩金池配置名称
|
const lotteryConfigNameFormatter = (row: any) => {
|
||||||
const lotteryConfigNameFormatter = (row: any) =>
|
const label = lotteryPoolRowLabel(row)
|
||||||
row?.diceLotteryPoolConfig?.name ??
|
if (label === '-' && !row?.lottery_config_id) {
|
||||||
(row?.lottery_config_id ? `#${row.lottery_config_id}` : t('page.table.customConfig'))
|
return t('page.table.customConfig')
|
||||||
|
}
|
||||||
|
return label
|
||||||
|
}
|
||||||
|
|
||||||
// 表格
|
// 表格
|
||||||
const {
|
const {
|
||||||
@@ -163,6 +227,8 @@
|
|||||||
} = useTable({
|
} = useTable({
|
||||||
core: {
|
core: {
|
||||||
apiFn: api.list,
|
apiFn: api.list,
|
||||||
|
excludeParams: ['create_time'],
|
||||||
|
immediate: !routeInit.hasFilter,
|
||||||
columnsFactory: () => [
|
columnsFactory: () => [
|
||||||
{ type: 'selection' },
|
{ type: 'selection' },
|
||||||
{ prop: 'username', label: 'page.table.username', align: 'center' },
|
{ prop: 'username', label: 'page.table.username', align: 'center' },
|
||||||
@@ -296,4 +362,10 @@
|
|||||||
ElMessage.warning(msg)
|
ElMessage.warning(msg)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
useRecordRouteSync({
|
||||||
|
searchForm,
|
||||||
|
activeDatePreset,
|
||||||
|
onSearch: handleSearch
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -88,7 +88,7 @@
|
|||||||
<el-option
|
<el-option
|
||||||
v-for="item in lotteryConfigOptions"
|
v-for="item in lotteryConfigOptions"
|
||||||
:key="item.id"
|
:key="item.id"
|
||||||
:label="(item.name && String(item.name).trim()) || `#${item.id}`"
|
:label="lotteryPoolOptionLabel(item)"
|
||||||
:value="item.id"
|
:value="item.id"
|
||||||
/>
|
/>
|
||||||
</el-select>
|
</el-select>
|
||||||
@@ -97,12 +97,12 @@
|
|||||||
<el-form-item v-if="currentLotteryConfig" :label="$t('page.form.currentConfig')" class="current-config-block">
|
<el-form-item v-if="currentLotteryConfig" :label="$t('page.form.currentConfig')" class="current-config-block">
|
||||||
<div class="current-lottery-config">
|
<div class="current-lottery-config">
|
||||||
<div class="config-row">
|
<div class="config-row">
|
||||||
<span class="config-label">{{ $t('page.form.configLabelName') }}:</span>
|
<span class="config-label">{{ $t('page.form.configLabelPoolName') }}:</span>
|
||||||
<span>{{ currentLotteryConfig.name ?? '-' }}</span>
|
<span>{{ lotteryPoolDisplayLabel(currentLotteryConfig) }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="config-row">
|
<div class="config-row">
|
||||||
<span class="config-label">{{ $t('page.form.configLabelType') }}:</span>
|
<span class="config-label">{{ $t('page.form.configLabelCode') }}:</span>
|
||||||
<span>{{ lotteryConfigTypeText(currentLotteryConfig.name) }}</span>
|
<span>{{ currentLotteryConfig.name ?? '-' }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="config-row">
|
<div class="config-row">
|
||||||
<span class="config-label">{{ $t('page.form.configLabelWeights') }}:</span>
|
<span class="config-label">{{ $t('page.form.configLabelWeights') }}:</span>
|
||||||
@@ -183,8 +183,16 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import api from '../../../api/player/index'
|
import api from '../../../api/player/index'
|
||||||
import lotteryConfigApi from '../../../api/lottery_pool_config/index'
|
import lotteryConfigApi, {
|
||||||
|
parseLotteryPoolConfigOption,
|
||||||
|
type LotteryPoolConfigOption
|
||||||
|
} from '../../../api/lottery_pool_config/index'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import {
|
||||||
|
lotteryPoolDisplayLabel,
|
||||||
|
lotteryPoolOptionLabel,
|
||||||
|
type LotteryPoolOption
|
||||||
|
} from '@/views/plugin/dice/utils/lotteryPoolDisplay'
|
||||||
import { getChannelDeptRequestParams, withChannelDeptParams } from '@/composables/useChannelDeptScope'
|
import { getChannelDeptRequestParams, withChannelDeptParams } from '@/composables/useChannelDeptScope'
|
||||||
import { isSuperAdminUser } from '@/utils/channelLayout'
|
import { isSuperAdminUser } from '@/utils/channelLayout'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
@@ -272,7 +280,7 @@
|
|||||||
const formData = reactive({ ...initialFormData })
|
const formData = reactive({ ...initialFormData })
|
||||||
|
|
||||||
/** 彩金池配置下拉选项(DiceLotteryConfig id、name) */
|
/** 彩金池配置下拉选项(DiceLotteryConfig id、name) */
|
||||||
const lotteryConfigOptions = ref<Array<{ id: number; name: string }>>([])
|
const lotteryConfigOptions = ref<LotteryPoolOption[]>([])
|
||||||
/** 彩金池选项加载中 */
|
/** 彩金池选项加载中 */
|
||||||
const lotteryConfigLoading = ref(false)
|
const lotteryConfigLoading = ref(false)
|
||||||
/** 后台管理员下拉选项(SystemUser) */
|
/** 后台管理员下拉选项(SystemUser) */
|
||||||
@@ -298,7 +306,21 @@
|
|||||||
/** 管理员选项加载中 */
|
/** 管理员选项加载中 */
|
||||||
const systemUserOptionsLoading = ref(false)
|
const systemUserOptionsLoading = ref(false)
|
||||||
/** 当前选中的 DiceLotteryConfig 完整数据(用于展示) */
|
/** 当前选中的 DiceLotteryConfig 完整数据(用于展示) */
|
||||||
const currentLotteryConfig = ref<Record<string, any> | null>(null)
|
const currentLotteryConfig = ref<LotteryPoolConfigOption | null>(null)
|
||||||
|
|
||||||
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||||
|
return value !== null && typeof value === 'object'
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractReadPayload(res: unknown): Record<string, unknown> | null {
|
||||||
|
if (!isRecord(res)) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
if (isRecord(res.data)) {
|
||||||
|
return res.data
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
function lotteryConfigTypeText(name: unknown): string {
|
function lotteryConfigTypeText(name: unknown): string {
|
||||||
const n = String(name ?? '')
|
const n = String(name ?? '')
|
||||||
@@ -314,6 +336,15 @@
|
|||||||
return v == null || v === 0
|
return v == null || v === 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 将彩金池配置的 T1–T5 写入表单(绑定彩金池时展示与提交均以池为准) */
|
||||||
|
function applyPoolWeightsToForm(cfg: LotteryPoolConfigOption) {
|
||||||
|
formData.t1_weight = Number(cfg.t1_weight ?? 0)
|
||||||
|
formData.t2_weight = Number(cfg.t2_weight ?? 0)
|
||||||
|
formData.t3_weight = Number(cfg.t3_weight ?? 0)
|
||||||
|
formData.t4_weight = Number(cfg.t4_weight ?? 0)
|
||||||
|
formData.t5_weight = Number(cfg.t5_weight ?? 0)
|
||||||
|
}
|
||||||
|
|
||||||
/** 根据当前 lottery_config_id 加载 DiceLotteryConfig,并将五个权重写入当前 player.*_weight */
|
/** 根据当前 lottery_config_id 加载 DiceLotteryConfig,并将五个权重写入当前 player.*_weight */
|
||||||
async function loadCurrentLotteryConfig() {
|
async function loadCurrentLotteryConfig() {
|
||||||
const id = formData.lottery_config_id
|
const id = formData.lottery_config_id
|
||||||
@@ -323,12 +354,11 @@
|
|||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const res = await lotteryConfigApi.read(id)
|
const res = await lotteryConfigApi.read(id)
|
||||||
const row = (res as any)?.data ?? (res as any)
|
const payload = extractReadPayload(res)
|
||||||
if (row && typeof row === 'object') {
|
if (payload) {
|
||||||
|
const row = parseLotteryPoolConfigOption(payload)
|
||||||
currentLotteryConfig.value = row
|
currentLotteryConfig.value = row
|
||||||
WEIGHT_FIELDS.forEach((key) => {
|
applyPoolWeightsToForm(row)
|
||||||
;(formData as any)[key] = Number(row[key] ?? 0)
|
|
||||||
})
|
|
||||||
} else {
|
} else {
|
||||||
currentLotteryConfig.value = null
|
currentLotteryConfig.value = null
|
||||||
}
|
}
|
||||||
@@ -337,6 +367,15 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => currentLotteryConfig.value,
|
||||||
|
(cfg) => {
|
||||||
|
if (cfg && !isLotteryConfigEmpty()) {
|
||||||
|
applyPoolWeightsToForm(cfg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => props.modelValue,
|
() => props.modelValue,
|
||||||
(newVal) => {
|
(newVal) => {
|
||||||
@@ -352,11 +391,10 @@
|
|||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const res = await lotteryConfigApi.read(lotteryConfigId)
|
const res = await lotteryConfigApi.read(lotteryConfigId)
|
||||||
const row = (res as any)?.data ?? (res as any)
|
const payload = extractReadPayload(res)
|
||||||
if (row && typeof row === 'object') {
|
if (payload) {
|
||||||
WEIGHT_FIELDS.forEach((key) => {
|
const row = parseLotteryPoolConfigOption(payload)
|
||||||
;(formData as any)[key] = Number(row[key] ?? 0)
|
applyPoolWeightsToForm(row)
|
||||||
})
|
|
||||||
currentLotteryConfig.value = row
|
currentLotteryConfig.value = row
|
||||||
} else {
|
} else {
|
||||||
currentLotteryConfig.value = null
|
currentLotteryConfig.value = null
|
||||||
@@ -486,9 +524,12 @@
|
|||||||
ElMessage.warning(t('page.form.ruleWeightsSumMustBe100'))
|
ElMessage.warning(t('page.form.ruleWeightsSumMustBe100'))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if (!isLotteryConfigEmpty() && currentLotteryConfig.value) {
|
||||||
|
applyPoolWeightsToForm(currentLotteryConfig.value)
|
||||||
|
}
|
||||||
const payload = { ...formData }
|
const payload = { ...formData }
|
||||||
if (isLotteryConfigEmpty()) {
|
if (isLotteryConfigEmpty()) {
|
||||||
;(payload as any).lottery_config_id = null
|
payload.lottery_config_id = null
|
||||||
}
|
}
|
||||||
if (props.dialogType === 'edit' && !payload.password) {
|
if (props.dialogType === 'edit' && !payload.password) {
|
||||||
delete (payload as any).password
|
delete (payload as any).password
|
||||||
|
|||||||
@@ -54,17 +54,31 @@
|
|||||||
<el-option
|
<el-option
|
||||||
v-for="item in lotteryConfigOptions"
|
v-for="item in lotteryConfigOptions"
|
||||||
:key="item.id"
|
:key="item.id"
|
||||||
:label="item.name"
|
:label="lotteryPoolOptionLabel(item)"
|
||||||
:value="item.id"
|
:value="item.id"
|
||||||
/>
|
/>
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
|
<el-col v-bind="setSpan(8)">
|
||||||
|
<el-form-item :label="$t('page.search.createTime')" prop="create_time">
|
||||||
|
<el-date-picker
|
||||||
|
v-model="formData.create_time"
|
||||||
|
type="datetimerange"
|
||||||
|
:range-separator="$t('table.searchBar.rangeSeparator')"
|
||||||
|
:start-placeholder="$t('table.searchBar.startTime')"
|
||||||
|
:end-placeholder="$t('table.searchBar.endTime')"
|
||||||
|
value-format="YYYY-MM-DD HH:mm:ss"
|
||||||
|
style="width: 100%"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
</sa-search-bar>
|
</sa-search-bar>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import api from '../../../api/player/index'
|
import api from '../../../api/player/index'
|
||||||
|
import { lotteryPoolOptionLabel, type LotteryPoolOption } from '@/views/plugin/dice/utils/lotteryPoolDisplay'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
modelValue: Record<string, any>
|
modelValue: Record<string, any>
|
||||||
@@ -77,7 +91,7 @@
|
|||||||
const props = defineProps<Props>()
|
const props = defineProps<Props>()
|
||||||
const emit = defineEmits<Emits>()
|
const emit = defineEmits<Emits>()
|
||||||
const isExpanded = ref<boolean>(false)
|
const isExpanded = ref<boolean>(false)
|
||||||
const lotteryConfigOptions = ref<Array<{ id: number; name: string }>>([])
|
const lotteryConfigOptions = ref<LotteryPoolOption[]>([])
|
||||||
|
|
||||||
/** 从玩家控制器获取 DiceLotteryPoolConfig id/name 列表,用于 lottery_config_id 筛选 */
|
/** 从玩家控制器获取 DiceLotteryPoolConfig id/name 列表,用于 lottery_config_id 筛选 */
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
<template>
|
<template>
|
||||||
<el-dialog
|
<el-dialog
|
||||||
v-model="visible"
|
v-model="visible"
|
||||||
:title="dialogType === 'add' ? $t('page.form.dialogTitleAdd') : $t('page.form.dialogTitleEdit')"
|
:title="$t('page.form.dialogTitleEdit')"
|
||||||
width="600px"
|
width="600px"
|
||||||
align-center
|
align-center
|
||||||
:close-on-click-modal="false"
|
:close-on-click-modal="false"
|
||||||
@close="handleClose"
|
@close="handleClose"
|
||||||
>
|
>
|
||||||
<el-form ref="formRef" :model="formData" :rules="rules" label-width="120px">
|
<el-form ref="formRef" :model="formData" label-width="120px">
|
||||||
<el-form-item :label="$t('page.form.player')" prop="player_id">
|
<el-form-item :label="$t('page.form.player')" prop="player_id">
|
||||||
<el-select
|
<el-select
|
||||||
v-model="formData.player_id"
|
v-model="formData.player_id"
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
clearable
|
clearable
|
||||||
filterable
|
filterable
|
||||||
style="width: 100%"
|
style="width: 100%"
|
||||||
:disabled="dialogType === 'edit'"
|
disabled
|
||||||
>
|
>
|
||||||
<el-option
|
<el-option
|
||||||
v-for="item in playerOptions"
|
v-for="item in playerOptions"
|
||||||
@@ -30,7 +30,7 @@
|
|||||||
v-model="formData.use_coins"
|
v-model="formData.use_coins"
|
||||||
:placeholder="$t('page.form.placeholderUseCoins')"
|
:placeholder="$t('page.form.placeholderUseCoins')"
|
||||||
:min="0"
|
:min="0"
|
||||||
:disabled="dialogType === 'edit'"
|
disabled
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item :label="$t('page.form.paidDrawCount')" prop="paid_ticket_count">
|
<el-form-item :label="$t('page.form.paidDrawCount')" prop="paid_ticket_count">
|
||||||
@@ -38,8 +38,7 @@
|
|||||||
v-model="formData.paid_ticket_count"
|
v-model="formData.paid_ticket_count"
|
||||||
:placeholder="$t('page.form.placeholderPaidDrawCount')"
|
:placeholder="$t('page.form.placeholderPaidDrawCount')"
|
||||||
:min="0"
|
:min="0"
|
||||||
@change="onTicketCountChange"
|
disabled
|
||||||
:disabled="dialogType === 'edit'"
|
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item :label="$t('page.form.freeDrawCount')" prop="free_ticket_count">
|
<el-form-item :label="$t('page.form.freeDrawCount')" prop="free_ticket_count">
|
||||||
@@ -47,8 +46,7 @@
|
|||||||
v-model="formData.free_ticket_count"
|
v-model="formData.free_ticket_count"
|
||||||
:placeholder="$t('page.form.placeholderFreeDrawCount')"
|
:placeholder="$t('page.form.placeholderFreeDrawCount')"
|
||||||
:min="0"
|
:min="0"
|
||||||
@change="onTicketCountChange"
|
disabled
|
||||||
:disabled="dialogType === 'edit'"
|
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item :label="$t('page.form.totalDrawCount')" prop="total_ticket_count">
|
<el-form-item :label="$t('page.form.totalDrawCount')" prop="total_ticket_count">
|
||||||
@@ -68,25 +66,20 @@
|
|||||||
maxlength="500"
|
maxlength="500"
|
||||||
show-word-limit
|
show-word-limit
|
||||||
style="width: 100%"
|
style="width: 100%"
|
||||||
:disabled="dialogType === 'edit'"
|
disabled
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<el-button @click="handleClose">{{ $t('common.cancel') }}</el-button>
|
<el-button @click="handleClose">{{ $t('form.close') }}</el-button>
|
||||||
<el-button type="primary" @click="handleSubmit">{{ $t('table.form.submit') }}</el-button>
|
|
||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import api from '../../../api/player_ticket_record/index'
|
import api from '../../../api/player_ticket_record/index'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { getChannelDeptRequestParams } from '@/composables/useChannelDeptScope'
|
||||||
import { getChannelDeptRequestParams, withChannelDeptParams } from '@/composables/useChannelDeptScope'
|
import type { FormInstance } from 'element-plus'
|
||||||
import { ElMessage } from 'element-plus'
|
|
||||||
import type { FormInstance, FormRules } from 'element-plus'
|
|
||||||
|
|
||||||
const { t } = useI18n()
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
modelValue: boolean
|
modelValue: boolean
|
||||||
@@ -101,7 +94,7 @@
|
|||||||
|
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
modelValue: false,
|
modelValue: false,
|
||||||
dialogType: 'add',
|
dialogType: 'edit',
|
||||||
data: undefined
|
data: undefined
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -117,17 +110,6 @@
|
|||||||
set: (value) => emit('update:modelValue', value)
|
set: (value) => emit('update:modelValue', value)
|
||||||
})
|
})
|
||||||
|
|
||||||
/**
|
|
||||||
* 表单验证规则
|
|
||||||
*/
|
|
||||||
const rules = reactive<FormRules>({
|
|
||||||
player_id: [{ required: true, message: t('page.form.rulePlayerRequired'), trigger: 'change' }],
|
|
||||||
use_coins: [{ required: true, message: t('page.form.ruleUseCoinsRequired'), trigger: 'blur' }],
|
|
||||||
paid_ticket_count: [{ required: true, message: t('page.form.rulePaidDrawRequired'), trigger: 'blur' }],
|
|
||||||
free_ticket_count: [{ required: true, message: t('page.form.ruleFreeDrawRequired'), trigger: 'blur' }],
|
|
||||||
remark: [{ required: true, message: t('page.form.ruleRemarkRequired'), trigger: 'blur' }]
|
|
||||||
})
|
|
||||||
|
|
||||||
/** 玩家下拉选项(id、username) */
|
/** 玩家下拉选项(id、username) */
|
||||||
const playerOptions = ref<Array<{ id: number; username: string }>>([])
|
const playerOptions = ref<Array<{ id: number; username: string }>>([])
|
||||||
|
|
||||||
@@ -138,10 +120,6 @@
|
|||||||
return paid + free
|
return paid + free
|
||||||
})
|
})
|
||||||
|
|
||||||
function onTicketCountChange() {
|
|
||||||
formData.total_ticket_count = totalTicketCountComputed.value
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 初始数据
|
* 初始数据
|
||||||
*/
|
*/
|
||||||
@@ -161,7 +139,7 @@
|
|||||||
const formData = reactive({ ...initialFormData })
|
const formData = reactive({ ...initialFormData })
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 监听弹窗打开,初始化表单并拉取玩家选项(与 player_wallet_record 一致)
|
* 监听弹窗打开,初始化表单并拉取玩家选项
|
||||||
*/
|
*/
|
||||||
watch(
|
watch(
|
||||||
() => props.modelValue,
|
() => props.modelValue,
|
||||||
@@ -182,7 +160,7 @@
|
|||||||
)
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 初始化页面数据(仅重置表单、回填编辑数据,不在此处请求玩家列表)
|
* 初始化页面数据
|
||||||
*/
|
*/
|
||||||
const initPage = async () => {
|
const initPage = async () => {
|
||||||
Object.assign(formData, { ...initialFormData })
|
Object.assign(formData, { ...initialFormData })
|
||||||
@@ -193,7 +171,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 初始化表单数据
|
* 回填表单数据
|
||||||
*/
|
*/
|
||||||
const initForm = () => {
|
const initForm = () => {
|
||||||
if (!props.data) return
|
if (!props.data) return
|
||||||
@@ -215,34 +193,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 关闭弹窗并重置表单
|
* 关闭弹窗
|
||||||
*/
|
*/
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
visible.value = false
|
visible.value = false
|
||||||
formRef.value?.resetFields()
|
formRef.value?.resetFields()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 提交表单(total_ticket_count 由 paid_ticket_count + free_ticket_count 自动求和,提交前写入)
|
|
||||||
*/
|
|
||||||
const handleSubmit = async () => {
|
|
||||||
if (!formRef.value) return
|
|
||||||
try {
|
|
||||||
formData.total_ticket_count = totalTicketCountComputed.value
|
|
||||||
await formRef.value.validate()
|
|
||||||
if (props.dialogType === 'add') {
|
|
||||||
const rest = { ...formData } as Record<string, unknown>
|
|
||||||
delete rest.id
|
|
||||||
await api.save(withChannelDeptParams(rest))
|
|
||||||
ElMessage.success(t('page.form.addSuccess'))
|
|
||||||
} else {
|
|
||||||
await api.update(withChannelDeptParams(formData))
|
|
||||||
ElMessage.success(t('page.form.editSuccess'))
|
|
||||||
}
|
|
||||||
emit('success')
|
|
||||||
handleClose()
|
|
||||||
} catch (error) {
|
|
||||||
console.log('表单验证失败:', error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="art-full-height">
|
<div class="art-full-height">
|
||||||
|
<QuickDateRangeBar v-model="activeDatePreset" @select="handleQuickDateSelect" />
|
||||||
<!-- 搜索面板 -->
|
<!-- 搜索面板 -->
|
||||||
<TableSearch v-model="searchForm" @search="handleSearch" @reset="resetSearchParams" />
|
<TableSearch v-model="searchForm" @search="handleSearch" @reset="handleResetSearch" />
|
||||||
|
|
||||||
<ElCard class="art-table-card" shadow="never">
|
<ElCard class="art-table-card" shadow="never">
|
||||||
<!-- 表格头部 -->
|
<!-- 表格头部 -->
|
||||||
@@ -11,6 +12,14 @@
|
|||||||
{{ $t('page.toolbar.coinChangeSummary') }}:<strong :class="coinSummaryClass">{{
|
{{ $t('page.toolbar.coinChangeSummary') }}:<strong :class="coinSummaryClass">{{
|
||||||
formatMoney2(totalCoinChange)
|
formatMoney2(totalCoinChange)
|
||||||
}}</strong>
|
}}</strong>
|
||||||
|
<template v-if="totalCoinInflow !== null && totalCoinOutflow !== null">
|
||||||
|
({{ $t('page.toolbar.coinInflow') }} <strong class="coin-summary-positive">{{
|
||||||
|
formatMoney2(totalCoinInflow)
|
||||||
|
}}</strong>
|
||||||
|
/ {{ $t('page.toolbar.coinOutflow') }} <strong class="coin-summary-negative">{{
|
||||||
|
formatMoney2(totalCoinOutflow)
|
||||||
|
}}</strong>)
|
||||||
|
</template>
|
||||||
</span>
|
</span>
|
||||||
<!-- <ElSpace wrap>-->
|
<!-- <ElSpace wrap>-->
|
||||||
<!-- <ElButton-->
|
<!-- <ElButton-->
|
||||||
@@ -87,24 +96,52 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import { useRoute } from 'vue-router'
|
||||||
import { useTable } from '@/hooks/core/useTable'
|
import { useTable } from '@/hooks/core/useTable'
|
||||||
import { defaultResponseAdapter } from '@/utils/table/tableUtils'
|
import { defaultResponseAdapter } from '@/utils/table/tableUtils'
|
||||||
import { useSaiAdmin } from '@/composables/useSaiAdmin'
|
import { useSaiAdmin } from '@/composables/useSaiAdmin'
|
||||||
import api from '../../api/player_wallet_record/index'
|
import api from '../../api/player_wallet_record/index'
|
||||||
import TableSearch from './modules/table-search.vue'
|
import TableSearch from './modules/table-search.vue'
|
||||||
import EditDialog from './modules/edit-dialog.vue'
|
import EditDialog from './modules/edit-dialog.vue'
|
||||||
|
import QuickDateRangeBar from '@/views/plugin/dice/components/QuickDateRangeBar.vue'
|
||||||
|
import {
|
||||||
|
detectPresetFromRange,
|
||||||
|
type DatePresetKey
|
||||||
|
} from '@/views/plugin/dice/utils/dateRangePresets'
|
||||||
|
import { getRecordRouteInit, useRecordRouteSync } from '@/views/plugin/dice/composables/useRecordRouteQuery'
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
const routeInit = getRecordRouteInit(route.query, true)
|
||||||
|
const activeDatePreset = ref<DatePresetKey | null>(routeInit.activePreset)
|
||||||
|
|
||||||
// 搜索表单
|
// 搜索表单
|
||||||
const searchForm = ref({
|
const searchForm = ref({
|
||||||
type: undefined,
|
type: undefined as number | undefined,
|
||||||
username: undefined,
|
username: undefined,
|
||||||
coin_min: undefined,
|
coin_min: undefined,
|
||||||
coin_max: undefined,
|
coin_max: undefined,
|
||||||
create_time: undefined as [string, string] | undefined
|
create_time: routeInit.create_time
|
||||||
})
|
})
|
||||||
|
if (routeInit.type !== undefined) {
|
||||||
|
searchForm.value.type = routeInit.type
|
||||||
|
}
|
||||||
|
|
||||||
/** 当前筛选条件下平台币变化合计 */
|
/** 当前筛选条件下平台币净变化、流入、流出 */
|
||||||
const totalCoinChange = ref<number | null>(null)
|
const totalCoinChange = ref<number | null>(null)
|
||||||
|
const totalCoinInflow = ref<number | null>(null)
|
||||||
|
const totalCoinOutflow = ref<number | null>(null)
|
||||||
|
|
||||||
|
const applyCoinSummary = (res: Record<string, unknown> | undefined) => {
|
||||||
|
const summary = res?.total_coin_change
|
||||||
|
totalCoinChange.value =
|
||||||
|
summary !== undefined && summary !== null && summary !== '' ? Number(summary) : null
|
||||||
|
const inflow = res?.total_coin_inflow
|
||||||
|
totalCoinInflow.value =
|
||||||
|
inflow !== undefined && inflow !== null && inflow !== '' ? Number(inflow) : null
|
||||||
|
const outflow = res?.total_coin_outflow
|
||||||
|
totalCoinOutflow.value =
|
||||||
|
outflow !== undefined && outflow !== null && outflow !== '' ? Number(outflow) : null
|
||||||
|
}
|
||||||
|
|
||||||
const coinSummaryClass = computed(() => {
|
const coinSummaryClass = computed(() => {
|
||||||
if (totalCoinChange.value === null) return ''
|
if (totalCoinChange.value === null) return ''
|
||||||
@@ -128,9 +165,7 @@
|
|||||||
const reqId = ++summaryRequestSeq
|
const reqId = ++summaryRequestSeq
|
||||||
const res = await api.list(params)
|
const res = await api.list(params)
|
||||||
if (reqId === summaryRequestSeq) {
|
if (reqId === summaryRequestSeq) {
|
||||||
const summary = (res as Record<string, unknown> | undefined)?.total_coin_change
|
applyCoinSummary(res as Record<string, unknown> | undefined)
|
||||||
totalCoinChange.value =
|
|
||||||
summary !== undefined && summary !== null && summary !== '' ? Number(summary) : null
|
|
||||||
}
|
}
|
||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
@@ -155,6 +190,29 @@
|
|||||||
getData()
|
getData()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleQuickDateSelect = (range: [string, string], preset: DatePresetKey) => {
|
||||||
|
searchForm.value.create_time = range
|
||||||
|
activeDatePreset.value = preset
|
||||||
|
handleSearch({ ...searchForm.value })
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleResetSearch = () => {
|
||||||
|
activeDatePreset.value = null
|
||||||
|
searchForm.value.create_time = undefined
|
||||||
|
resetSearchParams()
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => searchForm.value.create_time,
|
||||||
|
(range) => {
|
||||||
|
if (Array.isArray(range) && range.length === 2) {
|
||||||
|
activeDatePreset.value = detectPresetFromRange([range[0], range[1]])
|
||||||
|
} else {
|
||||||
|
activeDatePreset.value = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
// 类型展示:0=充值 1=提现 2=购买抽奖次数 3=管理员加点 4=管理员扣点 5=抽奖
|
// 类型展示:0=充值 1=提现 2=购买抽奖次数 3=管理员加点 4=管理员扣点 5=抽奖
|
||||||
const typeFormatter = (row: Record<string, unknown>) => {
|
const typeFormatter = (row: Record<string, unknown>) => {
|
||||||
@@ -218,6 +276,7 @@
|
|||||||
apiFn: listApi,
|
apiFn: listApi,
|
||||||
apiParams: { limit: 100 },
|
apiParams: { limit: 100 },
|
||||||
excludeParams: ['create_time'],
|
excludeParams: ['create_time'],
|
||||||
|
immediate: !routeInit.hasFilter,
|
||||||
columnsFactory: () => [
|
columnsFactory: () => [
|
||||||
{ type: 'selection', align: 'center' },
|
{ type: 'selection', align: 'center' },
|
||||||
{ prop: 'id', label: 'page.table.id', width: 80, align: 'center' },
|
{ prop: 'id', label: 'page.table.id', width: 80, align: 'center' },
|
||||||
@@ -281,19 +340,22 @@
|
|||||||
},
|
},
|
||||||
hooks: {
|
hooks: {
|
||||||
onSuccess(_data, response) {
|
onSuccess(_data, response) {
|
||||||
const raw = response as unknown as Record<string, unknown>
|
applyCoinSummary(response as unknown as Record<string, unknown>)
|
||||||
const summary = raw?.total_coin_change
|
|
||||||
if (summary !== undefined && summary !== null && summary !== '') {
|
|
||||||
totalCoinChange.value = Number(summary)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
transform: {
|
transform: {
|
||||||
responseAdapter(response) {
|
responseAdapter(response) {
|
||||||
const raw = (response ?? {}) as Record<string, unknown>
|
const raw = (response ?? {}) as Record<string, unknown>
|
||||||
const base = defaultResponseAdapter(response)
|
const base = defaultResponseAdapter(response)
|
||||||
|
const extra = base as Record<string, unknown>
|
||||||
if (raw.total_coin_change !== undefined && raw.total_coin_change !== null) {
|
if (raw.total_coin_change !== undefined && raw.total_coin_change !== null) {
|
||||||
;(base as Record<string, unknown>).total_coin_change = raw.total_coin_change
|
extra.total_coin_change = raw.total_coin_change
|
||||||
|
}
|
||||||
|
if (raw.total_coin_inflow !== undefined && raw.total_coin_inflow !== null) {
|
||||||
|
extra.total_coin_inflow = raw.total_coin_inflow
|
||||||
|
}
|
||||||
|
if (raw.total_coin_outflow !== undefined && raw.total_coin_outflow !== null) {
|
||||||
|
extra.total_coin_outflow = raw.total_coin_outflow
|
||||||
}
|
}
|
||||||
return base
|
return base
|
||||||
}
|
}
|
||||||
@@ -311,6 +373,13 @@
|
|||||||
handleSelectionChange
|
handleSelectionChange
|
||||||
// selectedRows
|
// selectedRows
|
||||||
} = useSaiAdmin()
|
} = useSaiAdmin()
|
||||||
|
|
||||||
|
useRecordRouteSync({
|
||||||
|
searchForm,
|
||||||
|
activeDatePreset,
|
||||||
|
onSearch: handleSearch,
|
||||||
|
withType: true
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
<template>
|
<template>
|
||||||
<el-dialog
|
<el-dialog
|
||||||
v-model="visible"
|
v-model="visible"
|
||||||
:title="dialogType === 'add' ? $t('page.form.dialogTitleAdd') : $t('page.form.dialogTitleEdit')"
|
:title="$t('page.form.dialogTitleEdit')"
|
||||||
width="600px"
|
width="600px"
|
||||||
align-center
|
align-center
|
||||||
:close-on-click-modal="false"
|
:close-on-click-modal="false"
|
||||||
@close="handleClose"
|
@close="handleClose"
|
||||||
>
|
>
|
||||||
<el-form ref="formRef" :model="formData" :rules="rules" label-width="120px">
|
<el-form ref="formRef" :model="formData" label-width="120px">
|
||||||
<el-form-item :label="$t('page.form.user')" prop="player_id">
|
<el-form-item :label="$t('page.form.user')" prop="player_id">
|
||||||
<el-select
|
<el-select
|
||||||
v-model="formData.player_id"
|
v-model="formData.player_id"
|
||||||
@@ -15,8 +15,7 @@
|
|||||||
clearable
|
clearable
|
||||||
filterable
|
filterable
|
||||||
style="width: 100%"
|
style="width: 100%"
|
||||||
:disabled="dialogType === 'edit'"
|
disabled
|
||||||
@change="onPlayerChange"
|
|
||||||
>
|
>
|
||||||
<el-option
|
<el-option
|
||||||
v-for="item in playerOptions"
|
v-for="item in playerOptions"
|
||||||
@@ -32,7 +31,7 @@
|
|||||||
:placeholder="$t('page.form.placeholderType')"
|
:placeholder="$t('page.form.placeholderType')"
|
||||||
clearable
|
clearable
|
||||||
style="width: 100%"
|
style="width: 100%"
|
||||||
:disabled="dialogType === 'edit'"
|
disabled
|
||||||
>
|
>
|
||||||
<el-option :label="$t('page.form.typeRecharge')" :value="0" />
|
<el-option :label="$t('page.form.typeRecharge')" :value="0" />
|
||||||
<el-option :label="$t('page.form.typeWithdraw')" :value="1" />
|
<el-option :label="$t('page.form.typeWithdraw')" :value="1" />
|
||||||
@@ -48,8 +47,7 @@
|
|||||||
:precision="2"
|
:precision="2"
|
||||||
:step="1"
|
:step="1"
|
||||||
style="width: 100%"
|
style="width: 100%"
|
||||||
@change="onCoinChange"
|
disabled
|
||||||
:disabled="dialogType === 'edit'"
|
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item :label="$t('page.form.walletBefore')" prop="wallet_before">
|
<el-form-item :label="$t('page.form.walletBefore')" prop="wallet_before">
|
||||||
@@ -78,25 +76,20 @@
|
|||||||
:placeholder="$t('page.form.placeholderRemark')"
|
:placeholder="$t('page.form.placeholderRemark')"
|
||||||
maxlength="500"
|
maxlength="500"
|
||||||
show-word-limit
|
show-word-limit
|
||||||
:disabled="dialogType === 'edit'"
|
disabled
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<el-button @click="handleClose">{{ $t('common.cancel') }}</el-button>
|
<el-button @click="handleClose">{{ $t('form.close') }}</el-button>
|
||||||
<el-button type="primary" @click="handleSubmit">{{ $t('table.form.submit') }}</el-button>
|
|
||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import api from '../../../api/player_wallet_record/index'
|
import api from '../../../api/player_wallet_record/index'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { getChannelDeptRequestParams } from '@/composables/useChannelDeptScope'
|
||||||
import { getChannelDeptRequestParams, withChannelDeptParams } from '@/composables/useChannelDeptScope'
|
import type { FormInstance } from 'element-plus'
|
||||||
import { ElMessage } from 'element-plus'
|
|
||||||
import type { FormInstance, FormRules } from 'element-plus'
|
|
||||||
|
|
||||||
const { t } = useI18n()
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
modelValue: boolean
|
modelValue: boolean
|
||||||
@@ -111,7 +104,7 @@
|
|||||||
|
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
modelValue: false,
|
modelValue: false,
|
||||||
dialogType: 'add',
|
dialogType: 'edit',
|
||||||
data: undefined
|
data: undefined
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -127,12 +120,6 @@
|
|||||||
set: (value) => emit('update:modelValue', value)
|
set: (value) => emit('update:modelValue', value)
|
||||||
})
|
})
|
||||||
|
|
||||||
const rules = reactive<FormRules>({
|
|
||||||
player_id: [{ required: true, message: t('page.form.ruleUserRequired'), trigger: 'change' }],
|
|
||||||
coin: [{ required: true, message: t('page.form.ruleCoinRequired'), trigger: 'blur' }],
|
|
||||||
type: [{ required: true, message: t('page.form.ruleTypeRequired'), trigger: 'change' }]
|
|
||||||
})
|
|
||||||
|
|
||||||
const initialFormData: {
|
const initialFormData: {
|
||||||
id: number | null
|
id: number | null
|
||||||
player_id: number | null
|
player_id: number | null
|
||||||
@@ -153,36 +140,6 @@
|
|||||||
|
|
||||||
const formData = reactive({ ...initialFormData })
|
const formData = reactive({ ...initialFormData })
|
||||||
|
|
||||||
/** 选择用户后拉取当前平台币作为钱包操作前 */
|
|
||||||
async function onPlayerChange(playerId: number | null) {
|
|
||||||
if (playerId == null) {
|
|
||||||
formData.wallet_before = 0
|
|
||||||
calcWalletAfter()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const res = await api.getPlayerWalletBefore(playerId)
|
|
||||||
const before = res?.wallet_before ?? 0
|
|
||||||
formData.wallet_before = Number(before)
|
|
||||||
calcWalletAfter()
|
|
||||||
} catch {
|
|
||||||
formData.wallet_before = 0
|
|
||||||
calcWalletAfter()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 平台币变化时重算钱包操作后 */
|
|
||||||
function onCoinChange() {
|
|
||||||
calcWalletAfter()
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 钱包操作后 = 钱包操作前 + 平台币变化 */
|
|
||||||
function calcWalletAfter() {
|
|
||||||
const before = Number(formData.wallet_before) || 0
|
|
||||||
const coin = Number(formData.coin) || 0
|
|
||||||
formData.wallet_after = Number((before + coin).toFixed(2))
|
|
||||||
}
|
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => props.modelValue,
|
() => props.modelValue,
|
||||||
async (open) => {
|
async (open) => {
|
||||||
@@ -230,24 +187,4 @@
|
|||||||
visible.value = false
|
visible.value = false
|
||||||
formRef.value?.resetFields()
|
formRef.value?.resetFields()
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
|
||||||
if (!formRef.value) return
|
|
||||||
try {
|
|
||||||
await formRef.value.validate()
|
|
||||||
calcWalletAfter()
|
|
||||||
const payload = { ...formData }
|
|
||||||
if (props.dialogType === 'add') {
|
|
||||||
await api.save(withChannelDeptParams(payload))
|
|
||||||
ElMessage.success(t('page.form.addSuccess'))
|
|
||||||
} else {
|
|
||||||
await api.update(withChannelDeptParams(payload))
|
|
||||||
ElMessage.success(t('page.form.editSuccess'))
|
|
||||||
}
|
|
||||||
emit('success')
|
|
||||||
handleClose()
|
|
||||||
} catch (error) {
|
|
||||||
console.log('表单验证失败:', error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -78,13 +78,6 @@
|
|||||||
return api.list({ ...params, direction: currentDirection.value })
|
return api.list({ ...params, direction: currentDirection.value })
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatMoney2(val: unknown): string {
|
|
||||||
if (val === '' || val === null || val === undefined) return '-'
|
|
||||||
const n = typeof val === 'number' ? val : Number(val)
|
|
||||||
if (!Number.isFinite(n)) return '-'
|
|
||||||
return n.toFixed(2)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleSearch = (params: Record<string, any>) => {
|
const handleSearch = (params: Record<string, any>) => {
|
||||||
Object.assign(searchParams, { ...params, direction: currentDirection.value })
|
Object.assign(searchParams, { ...params, direction: currentDirection.value })
|
||||||
getData()
|
getData()
|
||||||
@@ -132,13 +125,6 @@
|
|||||||
align: 'center',
|
align: 'center',
|
||||||
showOverflowTooltip: true
|
showOverflowTooltip: true
|
||||||
},
|
},
|
||||||
{
|
|
||||||
prop: 'real_ev',
|
|
||||||
label: 'page.table.realEv',
|
|
||||||
width: 110,
|
|
||||||
align: 'center',
|
|
||||||
formatter: (row: Record<string, any>) => formatMoney2(row?.real_ev)
|
|
||||||
},
|
|
||||||
{ prop: 'remark', label: 'page.table.remark', minWidth: 80, align: 'center', showOverflowTooltip: true },
|
{ prop: 'remark', label: 'page.table.remark', minWidth: 80, align: 'center', showOverflowTooltip: true },
|
||||||
{ prop: 'weight', label: 'page.table.weight', width: 110, align: 'center' }
|
{ prop: 'weight', label: 'page.table.weight', width: 110, align: 'center' }
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
<el-tab-pane v-for="t in tierKeys" :key="t" :label="t" :name="t">
|
<el-tab-pane v-for="t in tierKeys" :key="t" :label="t" :name="t">
|
||||||
<div v-if="getTierItems(t).length === 0" class="empty-tip">{{ $t('page.weightShared.emptyTier') }}</div>
|
<div v-if="getTierItems(t).length === 0" class="empty-tip">{{ $t('page.weightShared.emptyTier') }}</div>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<div class="chart-wrap" v-if="t !== 'T4' && t !== 'T5'">
|
<div class="chart-wrap">
|
||||||
<div class="chart-row">
|
<div class="chart-row">
|
||||||
<ArtBarChart
|
<ArtBarChart
|
||||||
:x-axis-name="$t('page.weightShared.xAxisEndIndex')"
|
:x-axis-name="$t('page.weightShared.xAxisEndIndex')"
|
||||||
@@ -31,7 +31,7 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="weight-sum" v-if="t !== 'T4' && t !== 'T5'">
|
<div class="weight-sum">
|
||||||
{{
|
{{
|
||||||
$t('page.weightShared.sumLineDual', {
|
$t('page.weightShared.sumLineDual', {
|
||||||
cw: getTierSum(t, 'clockwise'),
|
cw: getTierSum(t, 'clockwise'),
|
||||||
@@ -39,7 +39,6 @@
|
|||||||
})
|
})
|
||||||
}}
|
}}
|
||||||
</div>
|
</div>
|
||||||
<div class="weight-sum weight-sum-t4t5" v-else>{{ $t('page.weightShared.t4t5NoteSingle') }}</div>
|
|
||||||
<el-table :data="getTierItems(t)" border size="small" class="weight-table">
|
<el-table :data="getTierItems(t)" border size="small" class="weight-table">
|
||||||
<el-table-column
|
<el-table-column
|
||||||
:label="$t('page.weightShared.colEndIndexId')"
|
:label="$t('page.weightShared.colEndIndexId')"
|
||||||
@@ -54,17 +53,6 @@
|
|||||||
width="80"
|
width="80"
|
||||||
align="center"
|
align="center"
|
||||||
/>
|
/>
|
||||||
<el-table-column
|
|
||||||
:label="$t('page.weightShared.colRealEv')"
|
|
||||||
prop="real_ev"
|
|
||||||
width="90"
|
|
||||||
align="center"
|
|
||||||
show-overflow-tooltip
|
|
||||||
>
|
|
||||||
<template #default="{ row }">
|
|
||||||
<span>{{ formatMoney2(row?.real_ev) }}</span>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column
|
<el-table-column
|
||||||
:label="$t('page.weightShared.colUiText')"
|
:label="$t('page.weightShared.colUiText')"
|
||||||
prop="ui_text"
|
prop="ui_text"
|
||||||
@@ -89,7 +77,6 @@
|
|||||||
:max="10000"
|
:max="10000"
|
||||||
:step="1"
|
:step="1"
|
||||||
size="small"
|
size="small"
|
||||||
:disabled="isWeightDisabled(row, t)"
|
|
||||||
class="weight-slider"
|
class="weight-slider"
|
||||||
@update:model-value="
|
@update:model-value="
|
||||||
(v: number | number[]) =>
|
(v: number | number[]) =>
|
||||||
@@ -106,7 +93,7 @@
|
|||||||
<el-button
|
<el-button
|
||||||
type="primary"
|
type="primary"
|
||||||
link
|
link
|
||||||
:disabled="isWeightDisabled(row, t) || getItemWeight(row, 'clockwise') <= 1"
|
:disabled="getItemWeight(row, 'clockwise') <= 1"
|
||||||
@click="
|
@click="
|
||||||
setItemWeightByRow(
|
setItemWeightByRow(
|
||||||
t,
|
t,
|
||||||
@@ -122,7 +109,6 @@
|
|||||||
:min="1"
|
:min="1"
|
||||||
:max="10000"
|
:max="10000"
|
||||||
:step="1"
|
:step="1"
|
||||||
:disabled="isWeightDisabled(row, t)"
|
|
||||||
controls-position="right"
|
controls-position="right"
|
||||||
size="small"
|
size="small"
|
||||||
class="weight-input"
|
class="weight-input"
|
||||||
@@ -140,7 +126,7 @@
|
|||||||
type="primary"
|
type="primary"
|
||||||
link
|
link
|
||||||
:disabled="
|
:disabled="
|
||||||
isWeightDisabled(row, t) || getItemWeight(row, 'clockwise') >= 10000
|
getItemWeight(row, 'clockwise') >= 10000
|
||||||
"
|
"
|
||||||
@click="
|
@click="
|
||||||
setItemWeightByRow(
|
setItemWeightByRow(
|
||||||
@@ -166,7 +152,6 @@
|
|||||||
:max="10000"
|
:max="10000"
|
||||||
:step="1"
|
:step="1"
|
||||||
size="small"
|
size="small"
|
||||||
:disabled="isWeightDisabled(row, t)"
|
|
||||||
class="weight-slider"
|
class="weight-slider"
|
||||||
@update:model-value="
|
@update:model-value="
|
||||||
(v: number | number[]) =>
|
(v: number | number[]) =>
|
||||||
@@ -184,7 +169,7 @@
|
|||||||
type="primary"
|
type="primary"
|
||||||
link
|
link
|
||||||
:disabled="
|
:disabled="
|
||||||
isWeightDisabled(row, t) || getItemWeight(row, 'counterclockwise') <= 1
|
getItemWeight(row, 'counterclockwise') <= 1
|
||||||
"
|
"
|
||||||
@click="
|
@click="
|
||||||
setItemWeightByRow(
|
setItemWeightByRow(
|
||||||
@@ -201,7 +186,6 @@
|
|||||||
:min="1"
|
:min="1"
|
||||||
:max="10000"
|
:max="10000"
|
||||||
:step="1"
|
:step="1"
|
||||||
:disabled="isWeightDisabled(row, t)"
|
|
||||||
controls-position="right"
|
controls-position="right"
|
||||||
size="small"
|
size="small"
|
||||||
class="weight-input"
|
class="weight-input"
|
||||||
@@ -219,7 +203,6 @@
|
|||||||
type="primary"
|
type="primary"
|
||||||
link
|
link
|
||||||
:disabled="
|
:disabled="
|
||||||
isWeightDisabled(row, t) ||
|
|
||||||
getItemWeight(row, 'counterclockwise') >= 10000
|
getItemWeight(row, 'counterclockwise') >= 10000
|
||||||
"
|
"
|
||||||
@click="
|
@click="
|
||||||
@@ -361,11 +344,6 @@
|
|||||||
else row[key] = v
|
else row[key] = v
|
||||||
}
|
}
|
||||||
|
|
||||||
function isWeightDisabled(row: WeightRow, tier: string): boolean {
|
|
||||||
if (tier === 'T4' || tier === 'T5') return true
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizeWeightValue(v: unknown): number {
|
function normalizeWeightValue(v: unknown): number {
|
||||||
const num = typeof v === 'number' && !Number.isNaN(v) ? v : Number(v)
|
const num = typeof v === 'number' && !Number.isNaN(v) ? v : Number(v)
|
||||||
if (Number.isNaN(num)) return 1
|
if (Number.isNaN(num)) return 1
|
||||||
@@ -465,8 +443,8 @@
|
|||||||
const items: Array<{ id: number; weight: number }> = []
|
const items: Array<{ id: number; weight: number }> = []
|
||||||
for (const t of TIER_KEYS) {
|
for (const t of TIER_KEYS) {
|
||||||
for (const row of getTierItems(t)) {
|
for (const row of getTierItems(t)) {
|
||||||
const w0 = isWeightDisabled(row, t) ? 10000 : getItemWeight(row, 'clockwise')
|
const w0 = getItemWeight(row, 'clockwise')
|
||||||
const w1 = isWeightDisabled(row, t) ? 10000 : getItemWeight(row, 'counterclockwise')
|
const w1 = getItemWeight(row, 'counterclockwise')
|
||||||
const rid0 = row.reward_id_clockwise != null ? row.reward_id_clockwise : 0
|
const rid0 = row.reward_id_clockwise != null ? row.reward_id_clockwise : 0
|
||||||
const rid1 = row.reward_id_counterclockwise != null ? row.reward_id_counterclockwise : 0
|
const rid1 = row.reward_id_counterclockwise != null ? row.reward_id_counterclockwise : 0
|
||||||
if (rid0 > 0) items.push({ id: rid0, weight: w0 })
|
if (rid0 > 0) items.push({ id: rid0, weight: w0 })
|
||||||
@@ -530,9 +508,6 @@
|
|||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
.weight-sum-t4t5 {
|
|
||||||
color: var(--el-text-color-secondary);
|
|
||||||
}
|
|
||||||
.global-tip {
|
.global-tip {
|
||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
padding: 10px 12px;
|
padding: 10px 12px;
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
<el-tab-pane v-for="t in tierKeys" :key="'cw-' + t" :label="t" :name="t">
|
<el-tab-pane v-for="t in tierKeys" :key="'cw-' + t" :label="t" :name="t">
|
||||||
<div v-if="getTierItems(t).length === 0" class="empty-tip">{{ $t('page.weightShared.emptyTier') }}</div>
|
<div v-if="getTierItems(t).length === 0" class="empty-tip">{{ $t('page.weightShared.emptyTier') }}</div>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<div class="chart-wrap" v-if="t !== 'T4' && t !== 'T5'">
|
<div class="chart-wrap">
|
||||||
<ArtBarChart
|
<ArtBarChart
|
||||||
:key="'cw-' + activeDirection + '-' + t"
|
:key="'cw-' + activeDirection + '-' + t"
|
||||||
:x-axis-name="$t('page.weightShared.xAxisGridNumber')"
|
:x-axis-name="$t('page.weightShared.xAxisGridNumber')"
|
||||||
@@ -27,12 +27,11 @@
|
|||||||
height="180px"
|
height="180px"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="weight-sum" v-if="t !== 'T4' && t !== 'T5'">
|
<div class="weight-sum">
|
||||||
{{
|
{{
|
||||||
$t('page.weightShared.sumLineSingle', { sum: getTierSumForCurrentDirection(t) })
|
$t('page.weightShared.sumLineSingle', { sum: getTierSumForCurrentDirection(t) })
|
||||||
}}
|
}}
|
||||||
</div>
|
</div>
|
||||||
<div class="weight-sum weight-sum-t4t5" v-else>{{ $t('page.weightShared.t4t5NoteSingle') }}</div>
|
|
||||||
<el-table :data="getTierItems(t)" border size="small" class="weight-table">
|
<el-table :data="getTierItems(t)" border size="small" class="weight-table">
|
||||||
<el-table-column
|
<el-table-column
|
||||||
:label="$t('page.weightShared.colGridNumber')"
|
:label="$t('page.weightShared.colGridNumber')"
|
||||||
@@ -48,17 +47,6 @@
|
|||||||
align="center"
|
align="center"
|
||||||
show-overflow-tooltip
|
show-overflow-tooltip
|
||||||
/>
|
/>
|
||||||
<el-table-column
|
|
||||||
:label="$t('page.weightShared.colRealEv')"
|
|
||||||
prop="real_ev"
|
|
||||||
width="90"
|
|
||||||
align="center"
|
|
||||||
show-overflow-tooltip
|
|
||||||
>
|
|
||||||
<template #default="{ row }">
|
|
||||||
<span>{{ formatMoney2(row?.real_ev) }}</span>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column
|
<el-table-column
|
||||||
:label="$t('page.weightShared.colUiText')"
|
:label="$t('page.weightShared.colUiText')"
|
||||||
prop="ui_text"
|
prop="ui_text"
|
||||||
@@ -87,7 +75,6 @@
|
|||||||
:max="10000"
|
:max="10000"
|
||||||
:step="1"
|
:step="1"
|
||||||
size="small"
|
size="small"
|
||||||
:disabled="isWeightDisabled(row, t)"
|
|
||||||
class="weight-slider"
|
class="weight-slider"
|
||||||
@update:model-value="
|
@update:model-value="
|
||||||
(v: number | number[]) =>
|
(v: number | number[]) =>
|
||||||
@@ -103,9 +90,7 @@
|
|||||||
<el-button
|
<el-button
|
||||||
type="primary"
|
type="primary"
|
||||||
link
|
link
|
||||||
:disabled="
|
:disabled="getItemWeightForCurrentDirection(row) <= 1"
|
||||||
isWeightDisabled(row, t) || getItemWeightForCurrentDirection(row) <= 1
|
|
||||||
"
|
|
||||||
@click="
|
@click="
|
||||||
setItemWeightForCurrentDirection(
|
setItemWeightForCurrentDirection(
|
||||||
t,
|
t,
|
||||||
@@ -120,7 +105,6 @@
|
|||||||
:min="1"
|
:min="1"
|
||||||
:max="10000"
|
:max="10000"
|
||||||
:step="1"
|
:step="1"
|
||||||
:disabled="isWeightDisabled(row, t)"
|
|
||||||
controls-position="right"
|
controls-position="right"
|
||||||
size="small"
|
size="small"
|
||||||
class="weight-input"
|
class="weight-input"
|
||||||
@@ -136,10 +120,7 @@
|
|||||||
<el-button
|
<el-button
|
||||||
type="primary"
|
type="primary"
|
||||||
link
|
link
|
||||||
:disabled="
|
:disabled="getItemWeightForCurrentDirection(row) >= 10000"
|
||||||
isWeightDisabled(row, t) ||
|
|
||||||
getItemWeightForCurrentDirection(row) >= 10000
|
|
||||||
"
|
|
||||||
@click="
|
@click="
|
||||||
setItemWeightForCurrentDirection(
|
setItemWeightForCurrentDirection(
|
||||||
t,
|
t,
|
||||||
@@ -163,7 +144,7 @@
|
|||||||
<el-tab-pane v-for="t in tierKeys" :key="'ccw-' + t" :label="t" :name="t">
|
<el-tab-pane v-for="t in tierKeys" :key="'ccw-' + t" :label="t" :name="t">
|
||||||
<div v-if="getTierItems(t).length === 0" class="empty-tip">{{ $t('page.weightShared.emptyTier') }}</div>
|
<div v-if="getTierItems(t).length === 0" class="empty-tip">{{ $t('page.weightShared.emptyTier') }}</div>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<div class="chart-wrap" v-if="t !== 'T4' && t !== 'T5'">
|
<div class="chart-wrap">
|
||||||
<ArtBarChart
|
<ArtBarChart
|
||||||
:key="'ccw-' + activeDirection + '-' + t"
|
:key="'ccw-' + activeDirection + '-' + t"
|
||||||
:x-axis-name="$t('page.weightShared.xAxisGridNumber')"
|
:x-axis-name="$t('page.weightShared.xAxisGridNumber')"
|
||||||
@@ -172,12 +153,11 @@
|
|||||||
height="180px"
|
height="180px"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="weight-sum" v-if="t !== 'T4' && t !== 'T5'">
|
<div class="weight-sum">
|
||||||
{{
|
{{
|
||||||
$t('page.weightShared.sumLineSingle', { sum: getTierSumForCurrentDirection(t) })
|
$t('page.weightShared.sumLineSingle', { sum: getTierSumForCurrentDirection(t) })
|
||||||
}}
|
}}
|
||||||
</div>
|
</div>
|
||||||
<div class="weight-sum weight-sum-t4t5" v-else>{{ $t('page.weightShared.t4t5NoteSingle') }}</div>
|
|
||||||
<el-table :data="getTierItems(t)" border size="small" class="weight-table">
|
<el-table :data="getTierItems(t)" border size="small" class="weight-table">
|
||||||
<el-table-column
|
<el-table-column
|
||||||
:label="$t('page.weightShared.colGridNumber')"
|
:label="$t('page.weightShared.colGridNumber')"
|
||||||
@@ -193,13 +173,6 @@
|
|||||||
align="center"
|
align="center"
|
||||||
show-overflow-tooltip
|
show-overflow-tooltip
|
||||||
/>
|
/>
|
||||||
<el-table-column
|
|
||||||
:label="$t('page.weightShared.colRealEv')"
|
|
||||||
prop="real_ev"
|
|
||||||
width="90"
|
|
||||||
align="center"
|
|
||||||
show-overflow-tooltip
|
|
||||||
/>
|
|
||||||
<el-table-column
|
<el-table-column
|
||||||
:label="$t('page.weightShared.colUiText')"
|
:label="$t('page.weightShared.colUiText')"
|
||||||
prop="ui_text"
|
prop="ui_text"
|
||||||
@@ -228,7 +201,6 @@
|
|||||||
:max="10000"
|
:max="10000"
|
||||||
:step="1"
|
:step="1"
|
||||||
size="small"
|
size="small"
|
||||||
:disabled="isWeightDisabled(row, t)"
|
|
||||||
class="weight-slider"
|
class="weight-slider"
|
||||||
@update:model-value="
|
@update:model-value="
|
||||||
(v: number | number[]) =>
|
(v: number | number[]) =>
|
||||||
@@ -244,9 +216,7 @@
|
|||||||
<el-button
|
<el-button
|
||||||
type="primary"
|
type="primary"
|
||||||
link
|
link
|
||||||
:disabled="
|
:disabled="getItemWeightForCurrentDirection(row) <= 1"
|
||||||
isWeightDisabled(row, t) || getItemWeightForCurrentDirection(row) <= 1
|
|
||||||
"
|
|
||||||
@click="
|
@click="
|
||||||
setItemWeightForCurrentDirection(
|
setItemWeightForCurrentDirection(
|
||||||
t,
|
t,
|
||||||
@@ -261,7 +231,6 @@
|
|||||||
:min="1"
|
:min="1"
|
||||||
:max="10000"
|
:max="10000"
|
||||||
:step="1"
|
:step="1"
|
||||||
:disabled="isWeightDisabled(row, t)"
|
|
||||||
controls-position="right"
|
controls-position="right"
|
||||||
size="small"
|
size="small"
|
||||||
class="weight-input"
|
class="weight-input"
|
||||||
@@ -277,10 +246,7 @@
|
|||||||
<el-button
|
<el-button
|
||||||
type="primary"
|
type="primary"
|
||||||
link
|
link
|
||||||
:disabled="
|
:disabled="getItemWeightForCurrentDirection(row) >= 10000"
|
||||||
isWeightDisabled(row, t) ||
|
|
||||||
getItemWeightForCurrentDirection(row) >= 10000
|
|
||||||
"
|
|
||||||
@click="
|
@click="
|
||||||
setItemWeightForCurrentDirection(
|
setItemWeightForCurrentDirection(
|
||||||
t,
|
t,
|
||||||
@@ -433,11 +399,6 @@
|
|||||||
grouped.value[tier][dir] = [...list]
|
grouped.value[tier][dir] = [...list]
|
||||||
}
|
}
|
||||||
|
|
||||||
function isWeightDisabled(row: WeightRow, tier: string): boolean {
|
|
||||||
if (tier === 'T4' || tier === 'T5') return true
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizeWeightValue(v: unknown): number {
|
function normalizeWeightValue(v: unknown): number {
|
||||||
const num = typeof v === 'number' && !Number.isNaN(v) ? v : Number(v)
|
const num = typeof v === 'number' && !Number.isNaN(v) ? v : Number(v)
|
||||||
if (Number.isNaN(num)) return 1
|
if (Number.isNaN(num)) return 1
|
||||||
@@ -506,7 +467,7 @@
|
|||||||
for (const row of list) {
|
for (const row of list) {
|
||||||
const rid = row.reward_id != null ? Number(row.reward_id) : 0
|
const rid = row.reward_id != null ? Number(row.reward_id) : 0
|
||||||
if (rid <= 0) continue
|
if (rid <= 0) continue
|
||||||
const w = isWeightDisabled(row, t) ? 10000 : toWeightPrecision(row.weight ?? 1)
|
const w = toWeightPrecision(row.weight ?? 1)
|
||||||
items.push({ id: rid, reward_id: rid, weight: w })
|
items.push({ id: rid, reward_id: rid, weight: w })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -567,9 +528,6 @@
|
|||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
.weight-sum-t4t5 {
|
|
||||||
color: var(--el-text-color-secondary);
|
|
||||||
}
|
|
||||||
.global-tip {
|
.global-tip {
|
||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
padding: 10px 12px;
|
padding: 10px 12px;
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<ElDialog
|
<ElDialog
|
||||||
v-model="visible"
|
v-model="visible"
|
||||||
:title="$t('page.weightTest.title')"
|
:title="$t('page.weightTest.title')"
|
||||||
width="920px"
|
width="960px"
|
||||||
top="4vh"
|
top="4vh"
|
||||||
class="weight-test-dialog"
|
class="weight-test-dialog"
|
||||||
:close-on-click-modal="false"
|
:close-on-click-modal="false"
|
||||||
@@ -20,7 +20,7 @@
|
|||||||
|
|
||||||
<ElForm :model="form" label-width="108px" class="weight-test-form">
|
<ElForm :model="form" label-width="108px" class="weight-test-form">
|
||||||
<ElRow :gutter="16">
|
<ElRow :gutter="16">
|
||||||
<ElCol :span="8">
|
<ElCol :span="12">
|
||||||
<ElFormItem :label="$t('page.weightTest.labelAnte')" prop="ante_config_id" required>
|
<ElFormItem :label="$t('page.weightTest.labelAnte')" prop="ante_config_id" required>
|
||||||
<ElSelect
|
<ElSelect
|
||||||
v-model="form.ante_config_id"
|
v-model="form.ante_config_id"
|
||||||
@@ -29,6 +29,10 @@
|
|||||||
style="width: 100%"
|
style="width: 100%"
|
||||||
@change="syncAnteFromSelect"
|
@change="syncAnteFromSelect"
|
||||||
>
|
>
|
||||||
|
<ElOption
|
||||||
|
:label="$t('page.weightTest.anteRandomOption')"
|
||||||
|
:value="RANDOM_ANTE_CONFIG_ID"
|
||||||
|
/>
|
||||||
<ElOption
|
<ElOption
|
||||||
v-for="item in anteOptions"
|
v-for="item in anteOptions"
|
||||||
:key="item.id"
|
:key="item.id"
|
||||||
@@ -38,28 +42,44 @@
|
|||||||
</ElSelect>
|
</ElSelect>
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
</ElCol>
|
</ElCol>
|
||||||
<ElCol :span="8">
|
</ElRow>
|
||||||
<ElFormItem :label="$t('page.weightTest.labelKillModeEnabled')" prop="kill_mode_enabled">
|
|
||||||
<ElSwitch v-model="form.kill_mode_enabled" />
|
<div class="kill-mode-panel">
|
||||||
</ElFormItem>
|
<div class="kill-mode-head">
|
||||||
</ElCol>
|
<div class="kill-mode-title">{{ $t('page.weightTest.killModePanelTitle') }}</div>
|
||||||
<ElCol :span="8">
|
<ElSwitch
|
||||||
<ElFormItem :label="$t('page.weightTest.labelTestSafetyLine')" prop="test_safety_line">
|
v-model="form.kill_mode_enabled"
|
||||||
|
:active-text="$t('page.weightTest.killModeSwitchOn')"
|
||||||
|
:inactive-text="$t('page.weightTest.killModeSwitchOff')"
|
||||||
|
inline-prompt
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div v-if="form.kill_mode_enabled" class="kill-mode-body">
|
||||||
|
<div class="kill-mode-field">
|
||||||
|
<div class="kill-mode-field-label">{{ $t('page.weightTest.labelTestSafetyLine') }}</div>
|
||||||
<ElInputNumber
|
<ElInputNumber
|
||||||
v-model="form.test_safety_line"
|
v-model="form.test_safety_line"
|
||||||
:min="0"
|
:min="0"
|
||||||
:step="100"
|
:step="100"
|
||||||
:disabled="!form.kill_mode_enabled"
|
|
||||||
controls-position="right"
|
controls-position="right"
|
||||||
style="width: 100%"
|
class="kill-mode-field-input"
|
||||||
/>
|
/>
|
||||||
</ElFormItem>
|
</div>
|
||||||
</ElCol>
|
<div class="kill-mode-hint">{{ $t('page.weightTest.testSafetyLineHint') }}</div>
|
||||||
</ElRow>
|
<div v-if="defaultPoolInfo" class="kill-mode-ref">
|
||||||
|
{{ $t('page.weightTest.poolProfitRef', {
|
||||||
|
profit: defaultPoolInfo.profit_amount,
|
||||||
|
line: defaultPoolInfo.safety_line
|
||||||
|
}) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else class="kill-mode-off-hint">{{ $t('page.weightTest.killModeOffHint') }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<ElRow :gutter="20" class="section-row">
|
<ElRow :gutter="20" class="section-row">
|
||||||
<ElCol :span="12">
|
<ElCol :span="12">
|
||||||
<div class="section-title">{{ $t('page.weightTest.sectionPaid') }}</div>
|
<div class="section-title">{{ $t('page.weightTest.sectionPaid') }}</div>
|
||||||
|
<ElForm :model="form" label-position="top" class="section-form">
|
||||||
<ElFormItem :label="$t('page.weightTest.labelLotteryTypePaid')" prop="paid_lottery_config_id">
|
<ElFormItem :label="$t('page.weightTest.labelLotteryTypePaid')" prop="paid_lottery_config_id">
|
||||||
<ElSelect
|
<ElSelect
|
||||||
v-model="form.paid_lottery_config_id"
|
v-model="form.paid_lottery_config_id"
|
||||||
@@ -69,12 +89,18 @@
|
|||||||
style="width: 100%"
|
style="width: 100%"
|
||||||
>
|
>
|
||||||
<ElOption
|
<ElOption
|
||||||
v-for="item in paidLotteryOptions"
|
v-for="item in lotteryOptions"
|
||||||
:key="item.id"
|
:key="'paid-pool-' + item.id"
|
||||||
:label="item.name"
|
:label="lotteryPoolOptionLabel(item)"
|
||||||
:value="item.id"
|
:value="item.id"
|
||||||
/>
|
/>
|
||||||
</ElSelect>
|
</ElSelect>
|
||||||
|
<div v-if="selectedPaidPool" class="pool-selected-hint">
|
||||||
|
{{ $t('page.weightTest.selectedPoolHint', { name: lotteryPoolDisplayLabel(selectedPaidPool) }) }}
|
||||||
|
</div>
|
||||||
|
<div v-if="selectedPaidPool" class="pool-weights-preview">
|
||||||
|
{{ poolTierWeightsText(selectedPaidPool) }}
|
||||||
|
</div>
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
<template v-if="form.paid_lottery_config_id == null">
|
<template v-if="form.paid_lottery_config_id == null">
|
||||||
<div class="tier-label">{{ $t('page.weightTest.tierProbHint') }}</div>
|
<div class="tier-label">{{ $t('page.weightTest.tierProbHint') }}</div>
|
||||||
@@ -100,6 +126,8 @@
|
|||||||
$t('page.weightTest.tierSumError', { sum: paidTierSum })
|
$t('page.weightTest.tierSumError', { sum: paidTierSum })
|
||||||
}}</div>
|
}}</div>
|
||||||
</template>
|
</template>
|
||||||
|
<ElRow :gutter="12">
|
||||||
|
<ElCol :span="12">
|
||||||
<ElFormItem :label="$t('page.weightTest.labelCwCount')" prop="paid_s_count" required>
|
<ElFormItem :label="$t('page.weightTest.labelCwCount')" prop="paid_s_count" required>
|
||||||
<ElSelect
|
<ElSelect
|
||||||
v-model="form.paid_s_count"
|
v-model="form.paid_s_count"
|
||||||
@@ -109,6 +137,8 @@
|
|||||||
<ElOption v-for="c in countOptions" :key="c" :label="String(c)" :value="c" />
|
<ElOption v-for="c in countOptions" :key="c" :label="String(c)" :value="c" />
|
||||||
</ElSelect>
|
</ElSelect>
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
|
</ElCol>
|
||||||
|
<ElCol :span="12">
|
||||||
<ElFormItem :label="$t('page.weightTest.labelCcwCount')" prop="paid_n_count" required>
|
<ElFormItem :label="$t('page.weightTest.labelCcwCount')" prop="paid_n_count" required>
|
||||||
<ElSelect
|
<ElSelect
|
||||||
v-model="form.paid_n_count"
|
v-model="form.paid_n_count"
|
||||||
@@ -119,9 +149,13 @@
|
|||||||
</ElSelect>
|
</ElSelect>
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
</ElCol>
|
</ElCol>
|
||||||
|
</ElRow>
|
||||||
|
</ElForm>
|
||||||
|
</ElCol>
|
||||||
|
|
||||||
<ElCol :span="12">
|
<ElCol :span="12">
|
||||||
<div class="section-title">{{ $t('page.weightTest.sectionFreeAfterPlayAgain') }}</div>
|
<div class="section-title">{{ $t('page.weightTest.sectionFreeAfterPlayAgain') }}</div>
|
||||||
|
<ElForm :model="form" label-position="top" class="section-form">
|
||||||
<ElFormItem :label="$t('page.weightTest.labelLotteryTypeFree')" prop="free_lottery_config_id">
|
<ElFormItem :label="$t('page.weightTest.labelLotteryTypeFree')" prop="free_lottery_config_id">
|
||||||
<ElSelect
|
<ElSelect
|
||||||
v-model="form.free_lottery_config_id"
|
v-model="form.free_lottery_config_id"
|
||||||
@@ -131,12 +165,18 @@
|
|||||||
style="width: 100%"
|
style="width: 100%"
|
||||||
>
|
>
|
||||||
<ElOption
|
<ElOption
|
||||||
v-for="item in freeLotteryOptions"
|
v-for="item in lotteryOptions"
|
||||||
:key="item.id"
|
:key="'free-pool-' + item.id"
|
||||||
:label="item.name"
|
:label="lotteryPoolOptionLabel(item)"
|
||||||
:value="item.id"
|
:value="item.id"
|
||||||
/>
|
/>
|
||||||
</ElSelect>
|
</ElSelect>
|
||||||
|
<div v-if="selectedFreePool" class="pool-selected-hint">
|
||||||
|
{{ $t('page.weightTest.selectedPoolHint', { name: lotteryPoolDisplayLabel(selectedFreePool) }) }}
|
||||||
|
</div>
|
||||||
|
<div v-if="selectedFreePool" class="pool-weights-preview">
|
||||||
|
{{ poolTierWeightsText(selectedFreePool) }}
|
||||||
|
</div>
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
<template v-if="form.free_lottery_config_id == null">
|
<template v-if="form.free_lottery_config_id == null">
|
||||||
<div class="tier-label">{{ $t('page.weightTest.tierProbHintFreeChain') }}</div>
|
<div class="tier-label">{{ $t('page.weightTest.tierProbHintFreeChain') }}</div>
|
||||||
@@ -162,6 +202,7 @@
|
|||||||
$t('page.weightTest.tierSumError', { sum: freeTierSum })
|
$t('page.weightTest.tierSumError', { sum: freeTierSum })
|
||||||
}}</div>
|
}}</div>
|
||||||
</template>
|
</template>
|
||||||
|
</ElForm>
|
||||||
</ElCol>
|
</ElCol>
|
||||||
</ElRow>
|
</ElRow>
|
||||||
</ElForm>
|
</ElForm>
|
||||||
@@ -185,7 +226,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import api from '../../../api/reward/index'
|
import api from '../../../api/reward/index'
|
||||||
import anteConfigApi from '../../../api/ante_config/index'
|
import anteConfigApi from '../../../api/ante_config/index'
|
||||||
import lotteryPoolApi from '../../../api/lottery_pool_config/index'
|
import lotteryPoolApi, { type LotteryPoolConfigOption } from '../../../api/lottery_pool_config/index'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import {
|
import {
|
||||||
@@ -193,6 +234,13 @@
|
|||||||
useInjectedChannelDept,
|
useInjectedChannelDept,
|
||||||
withChannelDeptParams
|
withChannelDeptParams
|
||||||
} from '@/composables/useChannelDeptScope'
|
} from '@/composables/useChannelDeptScope'
|
||||||
|
import {
|
||||||
|
lotteryPoolDisplayLabel,
|
||||||
|
lotteryPoolOptionLabel
|
||||||
|
} from '@/views/plugin/dice/utils/lotteryPoolDisplay'
|
||||||
|
|
||||||
|
/** 底注下拉「随机」选项值(非真实 ante_config.id) */
|
||||||
|
const RANDOM_ANTE_CONFIG_ID = -1
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
/** 父页面渠道栏选中值(弹窗 teleport 后 inject 可能失效) */
|
/** 父页面渠道栏选中值(弹窗 teleport 后 inject 可能失效) */
|
||||||
@@ -222,16 +270,16 @@
|
|||||||
paid_s_count: 100,
|
paid_s_count: 100,
|
||||||
paid_n_count: 100,
|
paid_n_count: 100,
|
||||||
kill_mode_enabled: false,
|
kill_mode_enabled: false,
|
||||||
test_safety_line: 5000
|
test_safety_line: 0
|
||||||
})
|
})
|
||||||
const lotteryOptions = ref<Array<{ id: number; name: string }>>([])
|
const lotteryOptions = ref<LotteryPoolConfigOption[]>([])
|
||||||
const paidLotteryOptions = computed(() =>
|
const selectedPaidPool = computed(() =>
|
||||||
lotteryOptions.value.filter((r) => r.name === 'default')
|
lotteryOptions.value.find((r) => r.id === form.paid_lottery_config_id) ?? null
|
||||||
)
|
)
|
||||||
const freeLotteryOptions = computed(() => {
|
const selectedFreePool = computed(() =>
|
||||||
const list = lotteryOptions.value.filter((r) => r.name === 'killScore')
|
lotteryOptions.value.find((r) => r.id === form.free_lottery_config_id) ?? null
|
||||||
return list.length > 0 ? list : lotteryOptions.value
|
)
|
||||||
})
|
const defaultPoolInfo = ref<{ safety_line: number; kill_enabled: number; profit_amount: number } | null>(null)
|
||||||
const running = ref(false)
|
const running = ref(false)
|
||||||
|
|
||||||
function onClose() {
|
function onClose() {
|
||||||
@@ -296,6 +344,15 @@
|
|||||||
return label ? `${label} (×${item.mult})` : `×${item.mult}`
|
return label ? `${label} (×${item.mult})` : `×${item.mult}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function poolTierWeightsText(pool: LotteryPoolConfigOption): string {
|
||||||
|
const parts = tierKeys.map((t) => {
|
||||||
|
const key = `${t.toLowerCase()}_weight` as keyof LotteryPoolConfigOption
|
||||||
|
const v = pool[key]
|
||||||
|
return `${t} ${v ?? 0}%`
|
||||||
|
})
|
||||||
|
return parts.join(' · ')
|
||||||
|
}
|
||||||
|
|
||||||
function syncAnteFromSelect() {
|
function syncAnteFromSelect() {
|
||||||
const opt = anteOptions.value.find((o) => o.id === form.ante_config_id)
|
const opt = anteOptions.value.find((o) => o.id === form.ante_config_id)
|
||||||
if (opt) {
|
if (opt) {
|
||||||
@@ -321,20 +378,35 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function loadDefaultPoolInfo() {
|
||||||
|
try {
|
||||||
|
const pool = await lotteryPoolApi.getCurrentPool(resolveDeptParams())
|
||||||
|
const safetyLine = Number(pool?.safety_line ?? 0)
|
||||||
|
defaultPoolInfo.value = {
|
||||||
|
safety_line: safetyLine,
|
||||||
|
kill_enabled: Number(pool?.kill_enabled ?? 1),
|
||||||
|
profit_amount: Number(pool?.profit_amount ?? 0)
|
||||||
|
}
|
||||||
|
form.test_safety_line = safetyLine
|
||||||
|
} catch {
|
||||||
|
defaultPoolInfo.value = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function loadLotteryOptions() {
|
async function loadLotteryOptions() {
|
||||||
try {
|
try {
|
||||||
const list = await lotteryPoolApi.getOptions(resolveDeptParams())
|
const list = await lotteryPoolApi.getOptions(resolveDeptParams())
|
||||||
lotteryOptions.value = list.map((r: { id: number; name: string }) => ({
|
lotteryOptions.value = list
|
||||||
id: r.id,
|
const playerDefault = list.find((r) => r.name === 'playerDefault')
|
||||||
name: r.name
|
const normal = list.find((r) => r.name === 'default')
|
||||||
}))
|
if (playerDefault) {
|
||||||
const normal = list.find((r: { name?: string }) => r.name === 'default')
|
form.paid_lottery_config_id = playerDefault.id
|
||||||
if (normal) {
|
} else if (normal) {
|
||||||
form.paid_lottery_config_id = normal.id
|
form.paid_lottery_config_id = normal.id
|
||||||
}
|
}
|
||||||
const kill = list.find((r: { name?: string }) => r.name === 'killScore')
|
const freePool = list.find((r: { name?: string }) => r.name === 'free')
|
||||||
if (kill) {
|
if (freePool) {
|
||||||
form.free_lottery_config_id = kill.id
|
form.free_lottery_config_id = freePool.id
|
||||||
} else if (list.length > 0) {
|
} else if (list.length > 0) {
|
||||||
form.free_lottery_config_id = list[0].id
|
form.free_lottery_config_id = list[0].id
|
||||||
}
|
}
|
||||||
@@ -346,7 +418,6 @@
|
|||||||
function buildPayload() {
|
function buildPayload() {
|
||||||
const payload: Record<string, unknown> = {
|
const payload: Record<string, unknown> = {
|
||||||
ante: form.ante,
|
ante: form.ante,
|
||||||
ante_config_id: form.ante_config_id,
|
|
||||||
paid_s_count: form.paid_s_count,
|
paid_s_count: form.paid_s_count,
|
||||||
paid_n_count: form.paid_n_count,
|
paid_n_count: form.paid_n_count,
|
||||||
free_s_count: 0,
|
free_s_count: 0,
|
||||||
@@ -355,6 +426,11 @@
|
|||||||
kill_mode_enabled: form.kill_mode_enabled,
|
kill_mode_enabled: form.kill_mode_enabled,
|
||||||
test_safety_line: form.test_safety_line
|
test_safety_line: form.test_safety_line
|
||||||
}
|
}
|
||||||
|
if (form.ante_config_id === RANDOM_ANTE_CONFIG_ID) {
|
||||||
|
payload.ante_random = true
|
||||||
|
} else {
|
||||||
|
payload.ante_config_id = form.ante_config_id
|
||||||
|
}
|
||||||
if (form.paid_lottery_config_id != null) {
|
if (form.paid_lottery_config_id != null) {
|
||||||
payload.paid_lottery_config_id = form.paid_lottery_config_id
|
payload.paid_lottery_config_id = form.paid_lottery_config_id
|
||||||
} else {
|
} else {
|
||||||
@@ -369,12 +445,15 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function validateForm(): boolean {
|
function validateForm(): boolean {
|
||||||
if (form.ante_config_id == null || form.ante_config_id <= 0) {
|
const isRandomAnte = form.ante_config_id === RANDOM_ANTE_CONFIG_ID
|
||||||
|
if (!isRandomAnte && (form.ante_config_id == null || form.ante_config_id <= 0)) {
|
||||||
ElMessage.warning(t('page.weightTest.warnAnte'))
|
ElMessage.warning(t('page.weightTest.warnAnte'))
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
if (!isRandomAnte) {
|
||||||
syncAnteFromSelect()
|
syncAnteFromSelect()
|
||||||
if (form.ante == null || form.ante <= 0) {
|
}
|
||||||
|
if (!isRandomAnte && (form.ante == null || form.ante <= 0)) {
|
||||||
ElMessage.warning(t('page.weightTest.warnAnte'))
|
ElMessage.warning(t('page.weightTest.warnAnte'))
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -437,6 +516,7 @@
|
|||||||
if (v) {
|
if (v) {
|
||||||
void loadAnteOptions()
|
void loadAnteOptions()
|
||||||
void loadLotteryOptions()
|
void loadLotteryOptions()
|
||||||
|
void loadDefaultPoolInfo()
|
||||||
} else {
|
} else {
|
||||||
onClose()
|
onClose()
|
||||||
}
|
}
|
||||||
@@ -448,6 +528,7 @@
|
|||||||
if (visible.value) {
|
if (visible.value) {
|
||||||
void loadAnteOptions()
|
void loadAnteOptions()
|
||||||
void loadLotteryOptions()
|
void loadLotteryOptions()
|
||||||
|
void loadDefaultPoolInfo()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -478,6 +559,91 @@
|
|||||||
:deep(.el-form-item) {
|
:deep(.el-form-item) {
|
||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
}
|
}
|
||||||
|
:deep(.el-form-item__label) {
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-form {
|
||||||
|
:deep(.el-form-item) {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
:deep(.el-form-item__label) {
|
||||||
|
white-space: nowrap;
|
||||||
|
line-height: 1.4;
|
||||||
|
padding-bottom: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.kill-mode-panel {
|
||||||
|
margin-bottom: 14px;
|
||||||
|
padding: 12px 14px;
|
||||||
|
border: 1px solid var(--el-border-color-lighter);
|
||||||
|
border-radius: var(--el-border-radius-base);
|
||||||
|
background: var(--el-fill-color-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.kill-mode-head {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kill-mode-title {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--el-text-color-primary);
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kill-mode-body {
|
||||||
|
margin-top: 12px;
|
||||||
|
padding-top: 12px;
|
||||||
|
border-top: 1px dashed var(--el-border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pool-selected-hint {
|
||||||
|
margin-top: 6px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pool-weights-preview {
|
||||||
|
margin-top: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--el-text-color-regular);
|
||||||
|
font-family: ui-monospace, monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kill-mode-field {
|
||||||
|
max-width: 280px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kill-mode-field-label {
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--el-text-color-regular);
|
||||||
|
line-height: 1.4;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kill-mode-field-input {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kill-mode-hint,
|
||||||
|
.kill-mode-off-hint,
|
||||||
|
.kill-mode-ref {
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.5;
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.kill-mode-ref {
|
||||||
|
margin-top: 4px;
|
||||||
|
color: var(--el-text-color-placeholder);
|
||||||
}
|
}
|
||||||
|
|
||||||
.section-row {
|
.section-row {
|
||||||
@@ -491,6 +657,9 @@
|
|||||||
margin: 0 0 10px;
|
margin: 0 0 10px;
|
||||||
padding-bottom: 4px;
|
padding-bottom: 4px;
|
||||||
border-bottom: 1px solid var(--el-border-color-lighter);
|
border-bottom: 1px solid var(--el-border-color-lighter);
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tier-label {
|
.tier-label {
|
||||||
|
|||||||
@@ -21,9 +21,38 @@
|
|||||||
<ElTabPane :label="$t('page.configPage.tabIndex')" name="index">
|
<ElTabPane :label="$t('page.configPage.tabIndex')" name="index">
|
||||||
<div class="tab-panel">
|
<div class="tab-panel">
|
||||||
<div class="panel-tip">{{ $t('page.configPage.tipIndex') }}</div>
|
<div class="panel-tip">{{ $t('page.configPage.tipIndex') }}</div>
|
||||||
<div class="index-toolbar">
|
<div v-if="canTierRecommend" class="tier-recommend-panel">
|
||||||
|
<div class="tier-recommend-rules">{{ $t('page.configPage.tierRecommendRules') }}</div>
|
||||||
|
<div class="tier-recommend-grid">
|
||||||
|
<div v-for="tk in TIER_RECOMMEND_KEYS" :key="tk" class="tier-recommend-cell">
|
||||||
|
<span class="tier-recommend-label">{{ tk }}</span>
|
||||||
|
<span class="tier-recommend-hint">{{ $t('page.configPage.tierRecommendRealEv') }}</span>
|
||||||
|
<ElInputNumber
|
||||||
|
v-model="tierRecommend[tk]"
|
||||||
|
:disabled="tk === 'T5'"
|
||||||
|
:step="0.1"
|
||||||
|
:precision="2"
|
||||||
|
controls-position="right"
|
||||||
|
size="small"
|
||||||
|
class="tier-recommend-input"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="tier-recommend-actions">
|
||||||
|
<ElCheckbox v-model="autoMatchTierOnRealEv">{{
|
||||||
|
$t('page.configPage.tierRecommendAutoMatch')
|
||||||
|
}}</ElCheckbox>
|
||||||
|
<ElButton size="small" @click="handleApplyRecommendRealEv" v-ripple>{{
|
||||||
|
$t('page.configPage.tierRecommendApplyAmount')
|
||||||
|
}}</ElButton>
|
||||||
|
<ElButton size="small" type="primary" plain @click="handleMatchAllTiersFromRealEv" v-ripple>{{
|
||||||
|
$t('page.configPage.tierRecommendMatchTier')
|
||||||
|
}}</ElButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="canTierRecommend" class="index-toolbar">
|
||||||
<ElButton
|
<ElButton
|
||||||
v-permission="'dice:reward_config:index:batchUpdate'"
|
v-permission="PERM_TIER_RECOMMEND"
|
||||||
type="default"
|
type="default"
|
||||||
@click="openRuleGenerateDialog"
|
@click="openRuleGenerateDialog"
|
||||||
v-ripple
|
v-ripple
|
||||||
@@ -99,38 +128,18 @@
|
|||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<ElInputNumber
|
<ElInputNumber
|
||||||
v-model="row.real_ev"
|
v-model="row.real_ev"
|
||||||
@change="handleRealEvChange(row)"
|
|
||||||
controls-position="right"
|
controls-position="right"
|
||||||
size="small"
|
size="small"
|
||||||
:step="1"
|
:step="0.1"
|
||||||
|
:precision="2"
|
||||||
class="full-width"
|
class="full-width"
|
||||||
|
@update:model-value="() => handleRealEvChange(row)"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
</ElTableColumn>
|
</ElTableColumn>
|
||||||
<ElTableColumn
|
|
||||||
:label="$t('page.configPage.colRealReward')"
|
|
||||||
min-width="130"
|
|
||||||
align="center"
|
|
||||||
>
|
|
||||||
<template #default="{ row }">
|
|
||||||
<span>{{ formatMoney2(calcRealReward(row.real_ev)) }}</span>
|
|
||||||
</template>
|
|
||||||
</ElTableColumn>
|
|
||||||
<ElTableColumn :label="$t('page.configPage.colTier')" width="100" align="center">
|
<ElTableColumn :label="$t('page.configPage.colTier')" width="100" align="center">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<ElSelect
|
<span class="tier-readonly">{{ displayRowTier(row) }}</span>
|
||||||
v-model="row.tier"
|
|
||||||
:placeholder="$t('page.configPage.placeholderTierSelect')"
|
|
||||||
clearable
|
|
||||||
size="small"
|
|
||||||
class="full-width"
|
|
||||||
>
|
|
||||||
<ElOption label="T1" value="T1" />
|
|
||||||
<ElOption label="T2" value="T2" />
|
|
||||||
<ElOption label="T3" value="T3" />
|
|
||||||
<ElOption label="T4" value="T4" />
|
|
||||||
<ElOption label="T5" value="T5" />
|
|
||||||
</ElSelect>
|
|
||||||
</template>
|
</template>
|
||||||
</ElTableColumn>
|
</ElTableColumn>
|
||||||
<ElTableColumn
|
<ElTableColumn
|
||||||
@@ -142,7 +151,7 @@
|
|||||||
<ElInput
|
<ElInput
|
||||||
v-model="row.remark"
|
v-model="row.remark"
|
||||||
size="small"
|
size="small"
|
||||||
:placeholder="$t('page.configPage.placeholderRemark')"
|
:placeholder="remarkPlaceholderForRow(row)"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
</ElTableColumn>
|
</ElTableColumn>
|
||||||
@@ -167,6 +176,7 @@
|
|||||||
<ElTable
|
<ElTable
|
||||||
v-loading="loading"
|
v-loading="loading"
|
||||||
:data="bigwinRows"
|
:data="bigwinRows"
|
||||||
|
row-key="id"
|
||||||
border
|
border
|
||||||
size="default"
|
size="default"
|
||||||
class="config-table bigwin-table"
|
class="config-table bigwin-table"
|
||||||
@@ -214,11 +224,12 @@
|
|||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<ElInputNumber
|
<ElInputNumber
|
||||||
v-model="row.real_ev"
|
v-model="row.real_ev"
|
||||||
@change="handleRealEvChange(row)"
|
|
||||||
controls-position="right"
|
controls-position="right"
|
||||||
size="small"
|
size="small"
|
||||||
:step="1"
|
:step="0.1"
|
||||||
|
:precision="2"
|
||||||
class="full-width"
|
class="full-width"
|
||||||
|
@update:model-value="() => handleRealEvChange(row)"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
</ElTableColumn>
|
</ElTableColumn>
|
||||||
@@ -231,7 +242,7 @@
|
|||||||
<ElInput
|
<ElInput
|
||||||
v-model="row.remark"
|
v-model="row.remark"
|
||||||
size="small"
|
size="small"
|
||||||
:placeholder="$t('page.configPage.placeholderRemark')"
|
:placeholder="remarkPlaceholderForRow(row)"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
</ElTableColumn>
|
</ElTableColumn>
|
||||||
@@ -243,14 +254,17 @@
|
|||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<div class="weight-cell">
|
<div class="weight-cell">
|
||||||
<ElSlider
|
<ElSlider
|
||||||
v-model="row.weight"
|
:model-value="row.weight"
|
||||||
:min="0"
|
:min="0"
|
||||||
:max="10000"
|
:max="10000"
|
||||||
:step="100"
|
:step="100"
|
||||||
:disabled="isBigwinWeightDisabled(row)"
|
:disabled="isBigwinWeightDisabled(row)"
|
||||||
|
@update:model-value="
|
||||||
|
(v: number | number[]) => setBigwinRowWeight(row, Array.isArray(v) ? v[0] : v)
|
||||||
|
"
|
||||||
/>
|
/>
|
||||||
<ElInputNumber
|
<ElInputNumber
|
||||||
v-model="row.weight"
|
:model-value="row.weight"
|
||||||
:min="0"
|
:min="0"
|
||||||
:max="10000"
|
:max="10000"
|
||||||
:step="100"
|
:step="100"
|
||||||
@@ -258,6 +272,7 @@
|
|||||||
controls-position="right"
|
controls-position="right"
|
||||||
size="small"
|
size="small"
|
||||||
class="weight-input"
|
class="weight-input"
|
||||||
|
@update:model-value="(v: number | undefined) => setBigwinRowWeight(row, v)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<span v-if="isBigwinWeightDisabled(row)" class="weight-tip">{{
|
<span v-if="isBigwinWeightDisabled(row)" class="weight-tip">{{
|
||||||
@@ -286,6 +301,7 @@
|
|||||||
</ElCard>
|
</ElCard>
|
||||||
|
|
||||||
<ElDialog
|
<ElDialog
|
||||||
|
v-if="canTierRecommend"
|
||||||
v-model="ruleGenerateDialogVisible"
|
v-model="ruleGenerateDialogVisible"
|
||||||
:title="$t('page.configPage.ruleGenerateTitle')"
|
:title="$t('page.configPage.ruleGenerateTitle')"
|
||||||
:width="ruleGenDialogWidth"
|
:width="ruleGenDialogWidth"
|
||||||
@@ -393,6 +409,7 @@
|
|||||||
<ElInputNumber
|
<ElInputNumber
|
||||||
v-model="ruleGenT4RealEv"
|
v-model="ruleGenT4RealEv"
|
||||||
class="rule-gen-input-num"
|
class="rule-gen-input-num"
|
||||||
|
:disabled="true"
|
||||||
:step="1"
|
:step="1"
|
||||||
:controls="ruleGenInputControls"
|
:controls="ruleGenInputControls"
|
||||||
:size="ruleGenInputSize"
|
:size="ruleGenInputSize"
|
||||||
@@ -421,8 +438,8 @@
|
|||||||
<ElInputNumber
|
<ElInputNumber
|
||||||
v-model="ruleGenT5RealEv"
|
v-model="ruleGenT5RealEv"
|
||||||
class="rule-gen-input-num"
|
class="rule-gen-input-num"
|
||||||
:disabled="true"
|
:step="0.1"
|
||||||
:step="1"
|
:precision="2"
|
||||||
:controls="ruleGenInputControls"
|
:controls="ruleGenInputControls"
|
||||||
:size="ruleGenInputSize"
|
:size="ruleGenInputSize"
|
||||||
controls-position="right"
|
controls-position="right"
|
||||||
@@ -442,6 +459,12 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</ElDialog>
|
</ElDialog>
|
||||||
|
|
||||||
|
<CreateRewardReferencePreviewDialog
|
||||||
|
v-model="createRewardPreviewVisible"
|
||||||
|
:dept-id="filterDeptId"
|
||||||
|
@success="loadIndexList"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -453,20 +476,29 @@
|
|||||||
useInjectedChannelDept
|
useInjectedChannelDept
|
||||||
} from '@/composables/useChannelDeptScope'
|
} from '@/composables/useChannelDeptScope'
|
||||||
import { useWindowSize } from '@vueuse/core'
|
import { useWindowSize } from '@vueuse/core'
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import api from '../../api/reward_config/index'
|
import api from '../../api/reward_config/index'
|
||||||
|
import { checkAuth } from '@/utils/tool'
|
||||||
|
import CreateRewardReferencePreviewDialog from './modules/create-reward-reference-preview-dialog.vue'
|
||||||
import {
|
import {
|
||||||
buildRowsFromTiers,
|
buildRowsFromTiers,
|
||||||
computeBoardFrequencies,
|
computeBoardFrequencies,
|
||||||
DEFAULT_TIER_REAL_EV_STANDARDS,
|
DEFAULT_TIER_REAL_EV_STANDARDS,
|
||||||
|
defaultRemarkForTier,
|
||||||
generateTiers,
|
generateTiers,
|
||||||
|
inferTierFromRealEv,
|
||||||
summarizeCounts,
|
summarizeCounts,
|
||||||
|
type TierRealEvStandards,
|
||||||
validateTierRealEvStandards
|
validateTierRealEvStandards
|
||||||
} from '../utils/generateIndexByRules'
|
} from '../utils/generateIndexByRules'
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
|
|
||||||
|
/** 档位结算推荐配置(T1-T5 推荐金额栏、按规则生成/导入) */
|
||||||
|
const PERM_TIER_RECOMMEND = 'dice:reward_config:index:tierRecommend'
|
||||||
|
const canTierRecommend = computed(() => checkAuth(PERM_TIER_RECOMMEND))
|
||||||
|
|
||||||
const { width: viewportWidth } = useWindowSize()
|
const { width: viewportWidth } = useWindowSize()
|
||||||
/** 窄屏:单列、标签置顶、全屏弹窗 */
|
/** 窄屏:单列、标签置顶、全屏弹窗 */
|
||||||
const isRuleGenMobile = computed(() => viewportWidth.value < 640)
|
const isRuleGenMobile = computed(() => viewportWidth.value < 640)
|
||||||
@@ -510,18 +542,101 @@
|
|||||||
const savingIndex = ref(false)
|
const savingIndex = ref(false)
|
||||||
const savingBigwin = ref(false)
|
const savingBigwin = ref(false)
|
||||||
const createRewardLoading = ref(false)
|
const createRewardLoading = ref(false)
|
||||||
|
const createRewardPreviewVisible = ref(false)
|
||||||
const ruleGenerateDialogVisible = ref(false)
|
const ruleGenerateDialogVisible = ref(false)
|
||||||
const ruleGenSubmitting = ref(false)
|
const ruleGenSubmitting = ref(false)
|
||||||
const ruleGenT1Fixed = ref(3)
|
const ruleGenT1Fixed = ref(3)
|
||||||
const ruleGenT2Min = ref(5)
|
const ruleGenT2Min = ref(5)
|
||||||
const ruleGenT4Fixed = ref(1)
|
const ruleGenT4Fixed = ref(1)
|
||||||
const ruleGenT5Fixed = ref(1)
|
const ruleGenT5Fixed = ref(1)
|
||||||
|
const TIER_RECOMMEND_KEYS = ['T1', 'T2', 'T3', 'T4', 'T5'] as const
|
||||||
|
type TierRecommendKey = (typeof TIER_RECOMMEND_KEYS)[number]
|
||||||
|
|
||||||
|
const tierRecommend = reactive<TierRealEvStandards>({
|
||||||
|
T1: DEFAULT_TIER_REAL_EV_STANDARDS.T1,
|
||||||
|
T2: DEFAULT_TIER_REAL_EV_STANDARDS.T2,
|
||||||
|
T3: DEFAULT_TIER_REAL_EV_STANDARDS.T3,
|
||||||
|
T4: DEFAULT_TIER_REAL_EV_STANDARDS.T4,
|
||||||
|
T5: DEFAULT_TIER_REAL_EV_STANDARDS.T5
|
||||||
|
})
|
||||||
|
const autoMatchTierOnRealEv = ref(false)
|
||||||
|
|
||||||
const ruleGenT1RealEv = ref(DEFAULT_TIER_REAL_EV_STANDARDS.T1)
|
const ruleGenT1RealEv = ref(DEFAULT_TIER_REAL_EV_STANDARDS.T1)
|
||||||
const ruleGenT2RealEv = ref(DEFAULT_TIER_REAL_EV_STANDARDS.T2)
|
const ruleGenT2RealEv = ref(DEFAULT_TIER_REAL_EV_STANDARDS.T2)
|
||||||
const ruleGenT3RealEv = ref(DEFAULT_TIER_REAL_EV_STANDARDS.T3)
|
const ruleGenT3RealEv = ref(DEFAULT_TIER_REAL_EV_STANDARDS.T3)
|
||||||
const ruleGenT4RealEv = ref(DEFAULT_TIER_REAL_EV_STANDARDS.T4)
|
const ruleGenT4RealEv = ref(DEFAULT_TIER_REAL_EV_STANDARDS.T4)
|
||||||
const ruleGenT5RealEv = ref(DEFAULT_TIER_REAL_EV_STANDARDS.T5)
|
const ruleGenT5RealEv = ref(DEFAULT_TIER_REAL_EV_STANDARDS.T5)
|
||||||
|
|
||||||
|
function syncRuleGenFromTierRecommend() {
|
||||||
|
ruleGenT1RealEv.value = tierRecommend.T1
|
||||||
|
ruleGenT2RealEv.value = tierRecommend.T2
|
||||||
|
ruleGenT3RealEv.value = tierRecommend.T3
|
||||||
|
ruleGenT4RealEv.value = tierRecommend.T4
|
||||||
|
ruleGenT5RealEv.value = tierRecommend.T5
|
||||||
|
}
|
||||||
|
|
||||||
|
function syncTierRecommendFromRuleGen() {
|
||||||
|
tierRecommend.T1 = Number(ruleGenT1RealEv.value)
|
||||||
|
tierRecommend.T2 = Number(ruleGenT2RealEv.value)
|
||||||
|
tierRecommend.T3 = Number(ruleGenT3RealEv.value)
|
||||||
|
tierRecommend.T4 = Number(ruleGenT4RealEv.value)
|
||||||
|
tierRecommend.T5 = Number(ruleGenT5RealEv.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyRealEvDisplay(row: IndexRow, n: number) {
|
||||||
|
const text = Number.isNaN(n) ? '' : Number(n).toFixed(2)
|
||||||
|
row.ui_text = text
|
||||||
|
row.ui_text_en = text
|
||||||
|
}
|
||||||
|
|
||||||
|
function rowRealEvNumber(row: IndexRow): number {
|
||||||
|
return typeof row.real_ev === 'number' && !Number.isNaN(row.real_ev)
|
||||||
|
? row.real_ev
|
||||||
|
: Number(row.real_ev)
|
||||||
|
}
|
||||||
|
|
||||||
|
function syncRowTierFromRealEv(row: IndexRow) {
|
||||||
|
const tier = inferTierFromRealEv(rowRealEvNumber(row))
|
||||||
|
if (tier !== '') {
|
||||||
|
row.tier = tier
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 按当前结算金额推断档位并写入对应备注(T1大奖/T2小赚/T3抽水/T4惩罚/T5再来一次) */
|
||||||
|
function syncRemarkFromSettlement(row: IndexRow) {
|
||||||
|
const tier = inferTierFromRealEv(rowRealEvNumber(row))
|
||||||
|
if (tier === '') {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
row.remark = defaultRemarkForTier(tier)
|
||||||
|
}
|
||||||
|
|
||||||
|
function remarkPlaceholderForRow(row: IndexRow): string {
|
||||||
|
const tier = displayRowTier(row)
|
||||||
|
if (tier === '' || tier === '-') {
|
||||||
|
return t('page.configPage.placeholderRemark')
|
||||||
|
}
|
||||||
|
return defaultRemarkForTier(tier)
|
||||||
|
}
|
||||||
|
|
||||||
|
function displayRowTier(row: IndexRow): string {
|
||||||
|
const tier = inferTierFromRealEv(rowRealEvNumber(row))
|
||||||
|
return tier !== '' ? tier : row.tier || '-'
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyRecommendRealEvToRow(row: IndexRow, tier: TierRecommendKey) {
|
||||||
|
const ev = tierRecommend[tier]
|
||||||
|
row.real_ev = ev
|
||||||
|
if (tier === 'T5') {
|
||||||
|
row.ui_text = t('page.configPage.tierRecommendT5UiText')
|
||||||
|
row.ui_text_en = t('page.configPage.tierRecommendT5UiTextEn')
|
||||||
|
} else {
|
||||||
|
applyRealEvDisplay(row, ev)
|
||||||
|
}
|
||||||
|
syncRowTierFromRealEv(row)
|
||||||
|
syncRemarkFromSettlement(row)
|
||||||
|
}
|
||||||
|
|
||||||
/** 奖励索引 id 与后端 DiceRewardConfigLogic 一致:0~25 */
|
/** 奖励索引 id 与后端 DiceRewardConfigLogic 一致:0~25 */
|
||||||
const REWARD_INDEX_MIN = 0
|
const REWARD_INDEX_MIN = 0
|
||||||
const REWARD_INDEX_MAX = 25
|
const REWARD_INDEX_MAX = 25
|
||||||
@@ -564,22 +679,65 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function calcRealReward(realEv: unknown): number {
|
/** BIGWIN 行不参与按 real_ev 推断 T1-T5,避免编辑时从大奖权重表消失 */
|
||||||
const n = typeof realEv === 'number' && !Number.isNaN(realEv) ? realEv : Number(realEv)
|
function setBigwinRowWeight(row: IndexRow, v: number | number[] | undefined | null) {
|
||||||
if (Number.isNaN(n)) {
|
const n = Array.isArray(v) ? v[0] : v
|
||||||
return -1
|
row.weight = toWeight(n)
|
||||||
}
|
|
||||||
return n - 1
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleRealEvChange(row: IndexRow) {
|
function handleRealEvChange(row: IndexRow) {
|
||||||
const n =
|
if (row.tier === 'BIGWIN') {
|
||||||
typeof row.real_ev === 'number' && !Number.isNaN(row.real_ev)
|
row.remark = defaultRemarkForTier('BIGWIN')
|
||||||
? row.real_ev
|
return
|
||||||
: Number(row.real_ev)
|
}
|
||||||
const text = Number.isNaN(n) ? '' : Number(n).toFixed(2)
|
const n = rowRealEvNumber(row)
|
||||||
row.ui_text = text
|
syncRowTierFromRealEv(row)
|
||||||
row.ui_text_en = text
|
const tier = inferTierFromRealEv(n)
|
||||||
|
if (tier === 'T5') {
|
||||||
|
row.ui_text = t('page.configPage.tierRecommendT5UiText')
|
||||||
|
row.ui_text_en = t('page.configPage.tierRecommendT5UiTextEn')
|
||||||
|
} else {
|
||||||
|
applyRealEvDisplay(row, n)
|
||||||
|
}
|
||||||
|
syncRemarkFromSettlement(row)
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleApplyRecommendRealEv() {
|
||||||
|
if (!canTierRecommend.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let count = 0
|
||||||
|
for (const row of indexRowsExcludeBigwin.value) {
|
||||||
|
const tier = inferTierFromRealEv(rowRealEvNumber(row))
|
||||||
|
if (tier === 'T1' || tier === 'T2' || tier === 'T3' || tier === 'T4' || tier === 'T5') {
|
||||||
|
applyRecommendRealEvToRow(row, tier)
|
||||||
|
count++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (count === 0) {
|
||||||
|
ElMessage.info(t('page.configPage.tierRecommendNoTierRows'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ElMessage.success(t('page.configPage.tierRecommendApplyAmountOk', { n: count }))
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleMatchAllTiersFromRealEv() {
|
||||||
|
if (!canTierRecommend.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let count = 0
|
||||||
|
for (const row of indexRowsExcludeBigwin.value) {
|
||||||
|
syncRowTierFromRealEv(row)
|
||||||
|
syncRemarkFromSettlement(row)
|
||||||
|
if (row.tier !== '') {
|
||||||
|
count++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (count === 0) {
|
||||||
|
ElMessage.info(t('page.configPage.tierRecommendMatchTierNone'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ElMessage.success(t('page.configPage.tierRecommendMatchTierOk', { n: count }))
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatMoney2(val: unknown): string {
|
function formatMoney2(val: unknown): string {
|
||||||
@@ -590,43 +748,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function handleCreateRewardReference() {
|
async function handleCreateRewardReference() {
|
||||||
try {
|
createRewardPreviewVisible.value = true
|
||||||
await ElMessageBox.confirm(
|
|
||||||
t('page.configPage.confirmCreateRefMsg'),
|
|
||||||
t('page.configPage.confirmCreateRefTitle'),
|
|
||||||
{
|
|
||||||
confirmButtonText: t('page.configPage.confirmCreateRefOk'),
|
|
||||||
cancelButtonText: t('page.configPage.confirmCreateRefCancel'),
|
|
||||||
type: 'warning'
|
|
||||||
}
|
|
||||||
)
|
|
||||||
} catch {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
createRewardLoading.value = true
|
|
||||||
try {
|
|
||||||
const res: any = await api.createRewardReference({ dept_id: filterDeptId.value as number })
|
|
||||||
const data = res?.data ?? res
|
|
||||||
let msg = t('page.configPage.createRefSuccessSimple')
|
|
||||||
if (typeof data === 'object' && data !== null) {
|
|
||||||
const skipped = Number(data.skipped ?? 0)
|
|
||||||
const skippedPart =
|
|
||||||
skipped > 0 ? t('page.configPage.createRefSuccessSkipped', { n: skipped }) : ''
|
|
||||||
msg = t('page.configPage.createRefSuccess', {
|
|
||||||
cwNew: data.created_clockwise ?? 0,
|
|
||||||
ccwNew: data.created_counterclockwise ?? 0,
|
|
||||||
cwUp: data.updated_clockwise ?? 0,
|
|
||||||
ccwUp: data.updated_counterclockwise ?? 0,
|
|
||||||
skippedPart
|
|
||||||
})
|
|
||||||
}
|
|
||||||
ElMessage.success(msg)
|
|
||||||
loadIndexList()
|
|
||||||
} catch (e: any) {
|
|
||||||
ElMessage.error(e?.message ?? t('page.configPage.createRefFail'))
|
|
||||||
} finally {
|
|
||||||
createRewardLoading.value = false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function extractIndexList(res: unknown): Record<string, unknown>[] {
|
function extractIndexList(res: unknown): Record<string, unknown>[] {
|
||||||
@@ -651,6 +773,11 @@
|
|||||||
.list({ saiType: 'all', limit: 200, dept_id: filterDeptId.value })
|
.list({ saiType: 'all', limit: 200, dept_id: filterDeptId.value })
|
||||||
.then((res: unknown) => {
|
.then((res: unknown) => {
|
||||||
const rows = extractIndexList(res).map((r) => normalizeIndexRow(r))
|
const rows = extractIndexList(res).map((r) => normalizeIndexRow(r))
|
||||||
|
for (const row of rows) {
|
||||||
|
if (row.tier !== 'BIGWIN') {
|
||||||
|
syncRowTierFromRealEv(row)
|
||||||
|
}
|
||||||
|
}
|
||||||
indexRows.value = rows
|
indexRows.value = rows
|
||||||
indexRowsSnapshot = rows.map((r) => ({ ...r }))
|
indexRowsSnapshot = rows.map((r) => ({ ...r }))
|
||||||
})
|
})
|
||||||
@@ -745,6 +872,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function openRuleGenerateDialog() {
|
function openRuleGenerateDialog() {
|
||||||
|
if (!canTierRecommend.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
syncRuleGenFromTierRecommend()
|
||||||
ruleGenerateDialogVisible.value = true
|
ruleGenerateDialogVisible.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -781,6 +912,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function handleRuleGenerateApply() {
|
async function handleRuleGenerateApply() {
|
||||||
|
if (!canTierRecommend.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
const grids = extractGrids26()
|
const grids = extractGrids26()
|
||||||
if (grids === null) {
|
if (grids === null) {
|
||||||
ElMessage.warning(t('page.configPage.ruleGenNeedFullGrid'))
|
ElMessage.warning(t('page.configPage.ruleGenNeedFullGrid'))
|
||||||
@@ -790,12 +924,13 @@
|
|||||||
const t2 = Math.max(0, Math.min(26, Math.floor(Number(ruleGenT2Min.value))))
|
const t2 = Math.max(0, Math.min(26, Math.floor(Number(ruleGenT2Min.value))))
|
||||||
const x4 = Math.max(0, Math.min(26, Math.floor(Number(ruleGenT4Fixed.value))))
|
const x4 = Math.max(0, Math.min(26, Math.floor(Number(ruleGenT4Fixed.value))))
|
||||||
const x5 = Math.max(0, Math.min(26, Math.floor(Number(ruleGenT5Fixed.value))))
|
const x5 = Math.max(0, Math.min(26, Math.floor(Number(ruleGenT5Fixed.value))))
|
||||||
|
syncTierRecommendFromRuleGen()
|
||||||
const standards = {
|
const standards = {
|
||||||
T1: Number(ruleGenT1RealEv.value),
|
T1: Number(tierRecommend.T1),
|
||||||
T2: Number(ruleGenT2RealEv.value),
|
T2: Number(tierRecommend.T2),
|
||||||
T3: Number(ruleGenT3RealEv.value),
|
T3: Number(tierRecommend.T3),
|
||||||
T4: Number(ruleGenT4RealEv.value),
|
T4: Number(tierRecommend.T4),
|
||||||
T5: Number(ruleGenT5RealEv.value)
|
T5: Number(tierRecommend.T5)
|
||||||
}
|
}
|
||||||
const invalidKey = validateTierRealEvStandards(standards)
|
const invalidKey = validateTierRealEvStandards(standards)
|
||||||
if (invalidKey !== null) {
|
if (invalidKey !== null) {
|
||||||
@@ -850,7 +985,7 @@
|
|||||||
remark: r.remark
|
remark: r.remark
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
await api.batchUpdate(indexPayload, { dept_id: filterDeptId.value })
|
await api.generateIndexByRules(indexPayload, { dept_id: filterDeptId.value })
|
||||||
ElMessage.success(
|
ElMessage.success(
|
||||||
t('page.configPage.ruleGenSuccess', {
|
t('page.configPage.ruleGenSuccess', {
|
||||||
cwT1: sc.cw.T1,
|
cwT1: sc.cw.T1,
|
||||||
@@ -888,6 +1023,9 @@
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
const toSave = indexRows.value.filter((r) => r.tier !== 'BIGWIN')
|
const toSave = indexRows.value.filter((r) => r.tier !== 'BIGWIN')
|
||||||
|
for (const row of toSave) {
|
||||||
|
syncRowTierFromRealEv(row)
|
||||||
|
}
|
||||||
savingIndex.value = true
|
savingIndex.value = true
|
||||||
try {
|
try {
|
||||||
const indexPayload = toSave.map((r) => ({
|
const indexPayload = toSave.map((r) => ({
|
||||||
@@ -957,7 +1095,7 @@
|
|||||||
ui_text: r.ui_text,
|
ui_text: r.ui_text,
|
||||||
ui_text_en: r.ui_text_en,
|
ui_text_en: r.ui_text_en,
|
||||||
real_ev: r.real_ev,
|
real_ev: r.real_ev,
|
||||||
tier: r.tier,
|
tier: 'BIGWIN',
|
||||||
remark: r.remark
|
remark: r.remark
|
||||||
}))
|
}))
|
||||||
await api.batchUpdate(batchPayload, { dept_id: filterDeptId.value })
|
await api.batchUpdate(batchPayload, { dept_id: filterDeptId.value })
|
||||||
@@ -1058,6 +1196,61 @@
|
|||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
}
|
}
|
||||||
|
.tier-recommend-panel {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
padding: 12px 14px;
|
||||||
|
background: var(--el-fill-color-light);
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid var(--el-border-color-lighter);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.tier-recommend-rules {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
line-height: 1.6;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
.tier-recommend-grid {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 12px 20px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
.tier-recommend-cell {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px 8px;
|
||||||
|
min-width: 140px;
|
||||||
|
flex: 1 1 160px;
|
||||||
|
}
|
||||||
|
.tier-recommend-label {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--el-text-color-primary);
|
||||||
|
min-width: 28px;
|
||||||
|
}
|
||||||
|
.tier-recommend-hint {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.tier-recommend-input {
|
||||||
|
width: 120px;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 100px;
|
||||||
|
max-width: 160px;
|
||||||
|
}
|
||||||
|
.tier-readonly {
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--el-text-color-primary);
|
||||||
|
}
|
||||||
|
.tier-recommend-actions {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px 16px;
|
||||||
|
}
|
||||||
.index-toolbar {
|
.index-toolbar {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@@ -0,0 +1,343 @@
|
|||||||
|
<template>
|
||||||
|
<ElDialog
|
||||||
|
v-model="visible"
|
||||||
|
:title="$t('page.configPage.createRefPreviewTitle')"
|
||||||
|
width="860px"
|
||||||
|
align-center
|
||||||
|
:close-on-click-modal="false"
|
||||||
|
@close="handleClose"
|
||||||
|
>
|
||||||
|
<div v-loading="loading" class="dialog-body">
|
||||||
|
<div class="preview-tip">
|
||||||
|
<div v-if="meta.unchanged" class="tip-line">
|
||||||
|
{{ $t('page.configPage.createRefPreviewTipUnchanged') }}
|
||||||
|
</div>
|
||||||
|
<div v-else class="tip-line">
|
||||||
|
{{ $t('page.configPage.createRefPreviewTipChanged') }}
|
||||||
|
</div>
|
||||||
|
<div class="tip-line" v-if="meta.skipped > 0">
|
||||||
|
{{ $t('page.configPage.createRefPreviewSkipped', { n: meta.skipped }) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ElTabs v-model="activeDirection" type="card" class="direction-tabs">
|
||||||
|
<ElTabPane :label="$t('page.configPage.createRefPreviewClockwise')" name="0">
|
||||||
|
<ElTabs v-model="activeTier" type="card" class="tier-tabs">
|
||||||
|
<ElTabPane v-for="t in tierKeys" :key="'cw-' + t" :label="t" :name="t">
|
||||||
|
<ElTable
|
||||||
|
:data="getTierItems(t, 0)"
|
||||||
|
:row-class-name="rowClassName"
|
||||||
|
border
|
||||||
|
size="small"
|
||||||
|
class="preview-table"
|
||||||
|
>
|
||||||
|
<ElTableColumn :label="$t('page.table.dicePoints')" prop="grid_number" width="90" align="center" />
|
||||||
|
<ElTableColumn label="start" prop="start_index" width="78" align="center" />
|
||||||
|
<ElTableColumn :label="$t('page.table.endIndex')" prop="id" width="78" align="center" />
|
||||||
|
<ElTableColumn :label="$t('page.table.displayText')" prop="ui_text" width="90" align="center" show-overflow-tooltip />
|
||||||
|
<ElTableColumn :label="$t('page.table.remark')" prop="remark" min-width="80" align="center" show-overflow-tooltip />
|
||||||
|
<ElTableColumn :label="$t('page.table.weight')" width="130" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<ElInputNumber
|
||||||
|
v-model="row.weight"
|
||||||
|
:min="1"
|
||||||
|
:max="10000"
|
||||||
|
:step="1"
|
||||||
|
controls-position="right"
|
||||||
|
size="small"
|
||||||
|
class="weight-input"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</ElTableColumn>
|
||||||
|
<ElTableColumn :label="$t('page.configPage.createRefPreviewDiff')" min-width="120" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<span v-if="row?.diff_changed" class="diff-text">
|
||||||
|
{{ formatDiff(row) }}
|
||||||
|
</span>
|
||||||
|
<span v-else class="diff-text diff-text-ok">{{ $t('page.configPage.createRefPreviewNoDiff') }}</span>
|
||||||
|
</template>
|
||||||
|
</ElTableColumn>
|
||||||
|
</ElTable>
|
||||||
|
</ElTabPane>
|
||||||
|
</ElTabs>
|
||||||
|
</ElTabPane>
|
||||||
|
|
||||||
|
<ElTabPane :label="$t('page.configPage.createRefPreviewCounterclockwise')" name="1">
|
||||||
|
<ElTabs v-model="activeTier" type="card" class="tier-tabs">
|
||||||
|
<ElTabPane v-for="t in tierKeys" :key="'ccw-' + t" :label="t" :name="t">
|
||||||
|
<ElTable
|
||||||
|
:data="getTierItems(t, 1)"
|
||||||
|
:row-class-name="rowClassName"
|
||||||
|
border
|
||||||
|
size="small"
|
||||||
|
class="preview-table"
|
||||||
|
>
|
||||||
|
<ElTableColumn :label="$t('page.table.dicePoints')" prop="grid_number" width="90" align="center" />
|
||||||
|
<ElTableColumn label="start" prop="start_index" width="78" align="center" />
|
||||||
|
<ElTableColumn :label="$t('page.table.endIndex')" prop="id" width="78" align="center" />
|
||||||
|
<ElTableColumn :label="$t('page.table.displayText')" prop="ui_text" width="90" align="center" show-overflow-tooltip />
|
||||||
|
<ElTableColumn :label="$t('page.table.remark')" prop="remark" min-width="80" align="center" show-overflow-tooltip />
|
||||||
|
<ElTableColumn :label="$t('page.table.weight')" width="120" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<ElInputNumber
|
||||||
|
v-model="row.weight"
|
||||||
|
:min="1"
|
||||||
|
:max="10000"
|
||||||
|
:step="1"
|
||||||
|
controls-position="right"
|
||||||
|
size="small"
|
||||||
|
class="weight-input"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</ElTableColumn>
|
||||||
|
<ElTableColumn :label="$t('page.configPage.createRefPreviewDiff')" min-width="120" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<span v-if="row?.diff_changed" class="diff-text">
|
||||||
|
{{ formatDiff(row) }}
|
||||||
|
</span>
|
||||||
|
<span v-else class="diff-text diff-text-ok">{{ $t('page.configPage.createRefPreviewNoDiff') }}</span>
|
||||||
|
</template>
|
||||||
|
</ElTableColumn>
|
||||||
|
</ElTable>
|
||||||
|
</ElTabPane>
|
||||||
|
</ElTabs>
|
||||||
|
</ElTabPane>
|
||||||
|
</ElTabs>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<ElButton @click="refreshPreview" :loading="loading" v-ripple>{{
|
||||||
|
$t('page.configPage.createRefPreviewRefresh')
|
||||||
|
}}</ElButton>
|
||||||
|
<ElButton @click="handleClose" v-ripple>{{ $t('common.cancel') }}</ElButton>
|
||||||
|
<ElButton type="primary" :loading="submitting" @click="handleImport" v-ripple>{{
|
||||||
|
$t('page.configPage.createRefPreviewImport')
|
||||||
|
}}</ElButton>
|
||||||
|
</template>
|
||||||
|
</ElDialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import api from '../../../api/reward_config/index'
|
||||||
|
|
||||||
|
const { t } = useI18n()
|
||||||
|
|
||||||
|
const TIER_KEYS = ['T1', 'T2', 'T3', 'T4', 'T5'] as const
|
||||||
|
const tierKeys = TIER_KEYS
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
modelValue: boolean
|
||||||
|
deptId: number
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Emits {
|
||||||
|
(e: 'update:modelValue', value: boolean): void
|
||||||
|
(e: 'success'): void
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
modelValue: false,
|
||||||
|
deptId: 0
|
||||||
|
})
|
||||||
|
const emit = defineEmits<Emits>()
|
||||||
|
|
||||||
|
const visible = computed({
|
||||||
|
get: () => props.modelValue,
|
||||||
|
set: (v) => emit('update:modelValue', v)
|
||||||
|
})
|
||||||
|
|
||||||
|
const activeDirection = ref<'0' | '1'>('0')
|
||||||
|
const activeTier = ref('T1')
|
||||||
|
const loading = ref(false)
|
||||||
|
const submitting = ref(false)
|
||||||
|
|
||||||
|
const meta = reactive<{ unchanged: boolean; skipped: number }>({ unchanged: false, skipped: 0 })
|
||||||
|
const preview = ref<Record<string, { 0: any[]; 1: any[] }>>({})
|
||||||
|
|
||||||
|
function rowClassName({ row }: { row: any }): string {
|
||||||
|
return row?.diff_changed ? 'row-diff' : ''
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDiff(row: any): string {
|
||||||
|
if (!row) return ''
|
||||||
|
const oldStart = row.old_start_index != null ? String(row.old_start_index) : '-'
|
||||||
|
const oldEnd = row.old_end_index != null ? String(row.old_end_index) : '-'
|
||||||
|
const oldTier = row.old_tier != null ? String(row.old_tier) : '-'
|
||||||
|
const newStart = row.start_index != null ? String(row.start_index) : '-'
|
||||||
|
const newEnd = row.id != null ? String(row.id) : '-'
|
||||||
|
const newTier = row.tier != null ? String(row.tier) : '-'
|
||||||
|
return `${oldStart}/${oldEnd}/${oldTier} → ${newStart}/${newEnd}/${newTier}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTierItems(tier: string, direction: 0 | 1): any[] {
|
||||||
|
const tierData = preview.value?.[tier]
|
||||||
|
if (!tierData) return []
|
||||||
|
const rows = direction === 0 ? tierData[0] : tierData[1]
|
||||||
|
return Array.isArray(rows) ? rows : []
|
||||||
|
}
|
||||||
|
|
||||||
|
async function refreshPreview() {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const res: any = await api.createRewardReferencePreview({ dept_id: props.deptId })
|
||||||
|
const data = res?.data ?? res
|
||||||
|
meta.unchanged = Boolean(data?.unchanged)
|
||||||
|
meta.skipped = Number(data?.skipped ?? 0)
|
||||||
|
preview.value = (data?.preview ?? {}) as any
|
||||||
|
activeDirection.value = '0'
|
||||||
|
activeTier.value = 'T1'
|
||||||
|
} catch (e: any) {
|
||||||
|
ElMessage.error(e?.message ?? 'preview failed')
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toWeight(v: unknown): number {
|
||||||
|
const num = typeof v === 'number' && !Number.isNaN(v) ? v : Number(v)
|
||||||
|
if (Number.isNaN(num)) return 1
|
||||||
|
return Math.max(1, Math.min(10000, Math.floor(num)))
|
||||||
|
}
|
||||||
|
|
||||||
|
function collectDesiredWeights(): Record<string, number> {
|
||||||
|
const map: Record<string, number> = {}
|
||||||
|
for (const t of tierKeys) {
|
||||||
|
const tierData = preview.value?.[t]
|
||||||
|
if (!tierData) continue
|
||||||
|
const cw = Array.isArray(tierData[0]) ? tierData[0] : []
|
||||||
|
const ccw = Array.isArray(tierData[1]) ? tierData[1] : []
|
||||||
|
for (const r of cw) {
|
||||||
|
const gn = r?.grid_number != null ? Number(r.grid_number) : NaN
|
||||||
|
if (!Number.isNaN(gn)) map[`0:${gn}`] = toWeight(r?.weight)
|
||||||
|
}
|
||||||
|
for (const r of ccw) {
|
||||||
|
const gn = r?.grid_number != null ? Number(r.grid_number) : NaN
|
||||||
|
if (!Number.isNaN(gn)) map[`1:${gn}`] = toWeight(r?.weight)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map
|
||||||
|
}
|
||||||
|
|
||||||
|
async function applyWeights(desired: Record<string, number>) {
|
||||||
|
const res: any = await api.weightRatioList({ dept_id: props.deptId })
|
||||||
|
const raw = res?.data ?? res
|
||||||
|
const items: Array<{ id: number; weight: number }> = []
|
||||||
|
for (const t of tierKeys) {
|
||||||
|
const tierData = raw?.[t]
|
||||||
|
if (!tierData) continue
|
||||||
|
const list0 = Array.isArray(tierData[0]) ? tierData[0] : []
|
||||||
|
const list1 = Array.isArray(tierData[1]) ? tierData[1] : []
|
||||||
|
for (const r of list0) {
|
||||||
|
const rid = r?.reward_id != null ? Number(r.reward_id) : 0
|
||||||
|
const gn = r?.grid_number != null ? Number(r.grid_number) : NaN
|
||||||
|
if (rid > 0 && !Number.isNaN(gn)) {
|
||||||
|
const w = desired[`0:${gn}`]
|
||||||
|
if (w != null) items.push({ id: rid, weight: w })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const r of list1) {
|
||||||
|
const rid = r?.reward_id != null ? Number(r.reward_id) : 0
|
||||||
|
const gn = r?.grid_number != null ? Number(r.grid_number) : NaN
|
||||||
|
if (rid > 0 && !Number.isNaN(gn)) {
|
||||||
|
const w = desired[`1:${gn}`]
|
||||||
|
if (w != null) items.push({ id: rid, weight: w })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (items.length > 0) {
|
||||||
|
await api.batchUpdateWeights(items, { dept_id: props.deptId })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleImport() {
|
||||||
|
submitting.value = true
|
||||||
|
try {
|
||||||
|
const desired = collectDesiredWeights()
|
||||||
|
if (!meta.unchanged) {
|
||||||
|
await api.createRewardReference({ dept_id: props.deptId })
|
||||||
|
await applyWeights(desired)
|
||||||
|
ElMessage.success(t('page.configPage.createRefPreviewImportOk'))
|
||||||
|
} else {
|
||||||
|
// 映射未变化:直接保存权重即可
|
||||||
|
await applyWeights(desired)
|
||||||
|
ElMessage.success(t('page.configPage.createRefPreviewWeightsSaved'))
|
||||||
|
}
|
||||||
|
emit('success')
|
||||||
|
handleClose()
|
||||||
|
} catch (e: any) {
|
||||||
|
ElMessage.error(e?.message ?? 'import failed')
|
||||||
|
} finally {
|
||||||
|
submitting.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleClose() {
|
||||||
|
visible.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.modelValue,
|
||||||
|
(open) => {
|
||||||
|
if (open) {
|
||||||
|
refreshPreview()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.dialog-body {
|
||||||
|
min-height: 120px;
|
||||||
|
}
|
||||||
|
.preview-tip {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
padding: 8px 10px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--el-text-color-regular);
|
||||||
|
background: var(--el-fill-color-light);
|
||||||
|
border-radius: 6px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
.tip-line + .tip-line {
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
.preview-table {
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
.weight-input {
|
||||||
|
width: 110px;
|
||||||
|
}
|
||||||
|
:deep(.row-diff td) {
|
||||||
|
background: var(--el-color-warning-light-9);
|
||||||
|
}
|
||||||
|
.diff-text {
|
||||||
|
color: var(--el-text-color-regular);
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.2;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
.diff-text-ok {
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 减少弹窗内容区与表格留白 */
|
||||||
|
:deep(.el-dialog__body) {
|
||||||
|
padding: 12px 14px;
|
||||||
|
}
|
||||||
|
:deep(.el-tabs__header) {
|
||||||
|
margin: 0 0 8px 0;
|
||||||
|
}
|
||||||
|
:deep(.el-tabs__item) {
|
||||||
|
height: 34px;
|
||||||
|
line-height: 34px;
|
||||||
|
}
|
||||||
|
:deep(.el-table .cell) {
|
||||||
|
padding: 4px 6px;
|
||||||
|
}
|
||||||
|
:deep(.el-table__row) {
|
||||||
|
height: 34px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
@@ -50,13 +50,6 @@
|
|||||||
show-overflow-tooltip
|
show-overflow-tooltip
|
||||||
/>
|
/>
|
||||||
<el-table-column :label="$t('page.weightRatio.colDicePoints')" prop="grid_number" width="80" align="center" />
|
<el-table-column :label="$t('page.weightRatio.colDicePoints')" prop="grid_number" width="80" align="center" />
|
||||||
<el-table-column
|
|
||||||
:label="$t('page.weightRatio.colRealEv')"
|
|
||||||
prop="real_ev"
|
|
||||||
width="90"
|
|
||||||
align="center"
|
|
||||||
show-overflow-tooltip
|
|
||||||
/>
|
|
||||||
<el-table-column
|
<el-table-column
|
||||||
:label="$t('page.weightRatio.colUiText')"
|
:label="$t('page.weightRatio.colUiText')"
|
||||||
prop="ui_text"
|
prop="ui_text"
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ export interface TierRealEvStandards {
|
|||||||
T5: number
|
T5: number
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 默认标准(与规则弹窗说明一致) */
|
/** 默认推荐结算金额(满足档位区间规则,可在页面修改) */
|
||||||
export const DEFAULT_TIER_REAL_EV_STANDARDS: TierRealEvStandards = {
|
export const DEFAULT_TIER_REAL_EV_STANDARDS: TierRealEvStandards = {
|
||||||
T1: 3,
|
T1: 3,
|
||||||
T2: 1.5,
|
T2: 1.5,
|
||||||
@@ -62,6 +62,55 @@ export const DEFAULT_TIER_REAL_EV_STANDARDS: TierRealEvStandards = {
|
|||||||
T5: 0
|
T5: 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按结算金额推断档位(与奖励配置页规则说明一致)
|
||||||
|
* T1 大奖:>2
|
||||||
|
* T2 小赚:2>=金额>1
|
||||||
|
* T3 抽水:1>=金额>0
|
||||||
|
* T4 惩罚:0>金额
|
||||||
|
* T5 再来一次:=0
|
||||||
|
*/
|
||||||
|
/** 各档位默认备注(与结算金额推断档位规则一致,修改结算金额时实时同步) */
|
||||||
|
export const TIER_REMARK_BY_TIER: Record<IndexTier, string> = {
|
||||||
|
T1: '大奖',
|
||||||
|
T2: '小赚',
|
||||||
|
T3: '抽水',
|
||||||
|
T4: '惩罚',
|
||||||
|
T5: '再来一次'
|
||||||
|
}
|
||||||
|
|
||||||
|
export function defaultRemarkForTier(tier: IndexTier | 'BIGWIN' | string): string {
|
||||||
|
if (tier === 'BIGWIN' || tier === 'T1') {
|
||||||
|
return TIER_REMARK_BY_TIER.T1
|
||||||
|
}
|
||||||
|
if (tier === 'T2' || tier === 'T3' || tier === 'T4' || tier === 'T5') {
|
||||||
|
return TIER_REMARK_BY_TIER[tier]
|
||||||
|
}
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
export function inferTierFromRealEv(realEv: number): IndexTier | '' {
|
||||||
|
if (!Number.isFinite(realEv)) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
if (realEv === 0) {
|
||||||
|
return 'T5'
|
||||||
|
}
|
||||||
|
if (realEv < 0) {
|
||||||
|
return 'T4'
|
||||||
|
}
|
||||||
|
if (realEv > 2) {
|
||||||
|
return 'T1'
|
||||||
|
}
|
||||||
|
if (realEv > 1 && realEv <= 2) {
|
||||||
|
return 'T2'
|
||||||
|
}
|
||||||
|
if (realEv > 0 && realEv <= 1) {
|
||||||
|
return 'T3'
|
||||||
|
}
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 校验档位与 real_ev 区间是否一致;通过返回 null,否则返回 i18n 键名(不含 page.configPage. 前缀)
|
* 校验档位与 real_ev 区间是否一致;通过返回 null,否则返回 i18n 键名(不含 page.configPage. 前缀)
|
||||||
*/
|
*/
|
||||||
@@ -69,10 +118,10 @@ export function validateTierRealEvStandards(s: TierRealEvStandards): string | nu
|
|||||||
if (!Number.isFinite(s.T1) || !(s.T1 > 2)) {
|
if (!Number.isFinite(s.T1) || !(s.T1 > 2)) {
|
||||||
return 'ruleGenInvalidT1RealEv'
|
return 'ruleGenInvalidT1RealEv'
|
||||||
}
|
}
|
||||||
if (!Number.isFinite(s.T2) || !(s.T2 > 1 && s.T2 < 2)) {
|
if (!Number.isFinite(s.T2) || !(s.T2 > 1 && s.T2 <= 2)) {
|
||||||
return 'ruleGenInvalidT2RealEv'
|
return 'ruleGenInvalidT2RealEv'
|
||||||
}
|
}
|
||||||
if (!Number.isFinite(s.T3) || !(s.T3 > 0 && s.T3 < 1)) {
|
if (!Number.isFinite(s.T3) || !(s.T3 > 0 && s.T3 <= 1)) {
|
||||||
return 'ruleGenInvalidT3RealEv'
|
return 'ruleGenInvalidT3RealEv'
|
||||||
}
|
}
|
||||||
if (!Number.isFinite(s.T4) || !(s.T4 < 0)) {
|
if (!Number.isFinite(s.T4) || !(s.T4 < 0)) {
|
||||||
@@ -353,7 +402,7 @@ export function buildRowsFromTiers(
|
|||||||
const f = uiTextByTierWhenStandards(tier, real_ev)
|
const f = uiTextByTierWhenStandards(tier, real_ev)
|
||||||
ui_text = f.ui_text
|
ui_text = f.ui_text
|
||||||
ui_text_en = f.ui_text_en
|
ui_text_en = f.ui_text_en
|
||||||
remark = '前端需要在播放一次动画(特殊)'
|
remark = '再来一次'
|
||||||
}
|
}
|
||||||
} else if (tier === 'T1') {
|
} else if (tier === 'T1') {
|
||||||
real_ev = 101 + ((id * 17 + grid_number * 3) % 398)
|
real_ev = 101 + ((id * 17 + grid_number * 3) % 398)
|
||||||
@@ -393,10 +442,9 @@ export function buildRowsFromTiers(
|
|||||||
remark = '惩罚'
|
remark = '惩罚'
|
||||||
} else {
|
} else {
|
||||||
real_ev = 0
|
real_ev = 0
|
||||||
const f = uiTextFromRealEv(real_ev)
|
ui_text = '再来一次'
|
||||||
ui_text = f.ui_text
|
ui_text_en = 'Once again'
|
||||||
ui_text_en = f.ui_text_en
|
remark = '再来一次'
|
||||||
remark = '前端需要在播放一次动画(特殊)'
|
|
||||||
}
|
}
|
||||||
rows.push({
|
rows.push({
|
||||||
id,
|
id,
|
||||||
|
|||||||
@@ -143,6 +143,35 @@
|
|||||||
return Number(row.paid_n_count ?? 0)
|
return Number(row.paid_n_count ?? 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatTestSafetyLine(row: Record<string, unknown>): string {
|
||||||
|
const dash = t('page.detail.dash')
|
||||||
|
if (Number(row.kill_mode_enabled ?? 0) !== 1) {
|
||||||
|
return dash
|
||||||
|
}
|
||||||
|
const line = row.test_safety_line
|
||||||
|
if (line === null || line === undefined || line === '') {
|
||||||
|
return dash
|
||||||
|
}
|
||||||
|
const n = Number(line)
|
||||||
|
return Number.isFinite(n) ? String(n) : dash
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatAnteCell(row: Record<string, unknown>): string {
|
||||||
|
const snap = row.tier_weights_snapshot
|
||||||
|
const isRandom =
|
||||||
|
snap &&
|
||||||
|
typeof snap === 'object' &&
|
||||||
|
(snap as { ante_random?: boolean }).ante_random === true
|
||||||
|
if (isRandom) {
|
||||||
|
return t('page.table.anteRandom')
|
||||||
|
}
|
||||||
|
const ante = row.ante
|
||||||
|
if (ante === null || ante === undefined || ante === '') {
|
||||||
|
return t('page.detail.dash')
|
||||||
|
}
|
||||||
|
return String(ante)
|
||||||
|
}
|
||||||
|
|
||||||
// 平台赚取金额展示(未完成或空显示 —)
|
// 平台赚取金额展示(未完成或空显示 —)
|
||||||
function formatPlatformProfit(v: unknown): string {
|
function formatPlatformProfit(v: unknown): string {
|
||||||
const dash = t('page.detail.dash')
|
const dash = t('page.detail.dash')
|
||||||
@@ -228,8 +257,16 @@
|
|||||||
{
|
{
|
||||||
prop: 'ante',
|
prop: 'ante',
|
||||||
label: 'page.table.ante',
|
label: 'page.table.ante',
|
||||||
width: 90,
|
width: 100,
|
||||||
align: 'center'
|
align: 'center',
|
||||||
|
formatter: (row: Record<string, unknown>) => formatAnteCell(row)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
prop: 'test_safety_line',
|
||||||
|
label: 'page.table.testSafetyLine',
|
||||||
|
width: 100,
|
||||||
|
align: 'center',
|
||||||
|
formatter: (row: Record<string, unknown>) => formatTestSafetyLine(row)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
prop: 'play_again_count',
|
prop: 'play_again_count',
|
||||||
|
|||||||
@@ -20,6 +20,12 @@
|
|||||||
<el-descriptions-item :label="$t('page.detail.paidPlannedSpins')">
|
<el-descriptions-item :label="$t('page.detail.paidPlannedSpins')">
|
||||||
{{ record.paid_planned_spins ?? $t('page.detail.dash') }}
|
{{ record.paid_planned_spins ?? $t('page.detail.dash') }}
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item :label="$t('page.detail.testSafetyLine')">
|
||||||
|
{{ formatTestSafetyLineDetail(record) }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item :label="$t('page.table.ante')">
|
||||||
|
{{ formatAnteDetail(record) }}
|
||||||
|
</el-descriptions-item>
|
||||||
<el-descriptions-item :label="$t('page.detail.testCount')">
|
<el-descriptions-item :label="$t('page.detail.testCount')">
|
||||||
{{ formatTestCountDisplay(record) }}
|
{{ formatTestCountDisplay(record) }}
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
@@ -30,10 +36,10 @@
|
|||||||
{{ record.admin_name ?? record.admin_id ?? $t('page.detail.dash') }}
|
{{ record.admin_name ?? record.admin_id ?? $t('page.detail.dash') }}
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
<el-descriptions-item :label="$t('page.detail.paidPoolId')">
|
<el-descriptions-item :label="$t('page.detail.paidPoolId')">
|
||||||
{{ record.paid_lottery_config_id ?? record.lottery_config_id ?? $t('page.detail.dash') }}
|
{{ formatRecordPaidPoolName(record) }}
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
<el-descriptions-item :label="$t('page.detail.freePoolId')">
|
<el-descriptions-item :label="$t('page.detail.freePoolId')">
|
||||||
{{ record.free_lottery_config_id ?? $t('page.detail.dash') }}
|
{{ formatRecordFreePoolName(record) }}
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
<el-descriptions-item :label="$t('page.detail.bigwinSnapshot')">
|
<el-descriptions-item :label="$t('page.detail.bigwinSnapshot')">
|
||||||
<template v-if="bigwinWeightDisplay.length">
|
<template v-if="bigwinWeightDisplay.length">
|
||||||
@@ -182,7 +188,7 @@
|
|||||||
<el-option
|
<el-option
|
||||||
v-for="opt in paidLotteryOptions"
|
v-for="opt in paidLotteryOptions"
|
||||||
:key="opt.id"
|
:key="opt.id"
|
||||||
:label="opt.name"
|
:label="lotteryPoolOptionLabel(opt)"
|
||||||
:value="opt.id"
|
:value="opt.id"
|
||||||
/>
|
/>
|
||||||
</el-select>
|
</el-select>
|
||||||
@@ -199,7 +205,7 @@
|
|||||||
<el-option
|
<el-option
|
||||||
v-for="opt in freeLotteryOptions"
|
v-for="opt in freeLotteryOptions"
|
||||||
:key="opt.id"
|
:key="opt.id"
|
||||||
:label="opt.name"
|
:label="lotteryPoolOptionLabel(opt)"
|
||||||
:value="opt.id"
|
:value="opt.id"
|
||||||
/>
|
/>
|
||||||
</el-select>
|
</el-select>
|
||||||
@@ -224,6 +230,11 @@
|
|||||||
import ArtBarChart from '@/components/core/charts/art-bar-chart/index.vue'
|
import ArtBarChart from '@/components/core/charts/art-bar-chart/index.vue'
|
||||||
import recordApi from '../../../api/reward_config_record/index'
|
import recordApi from '../../../api/reward_config_record/index'
|
||||||
import lotteryConfigApi from '../../../api/lottery_pool_config/index'
|
import lotteryConfigApi from '../../../api/lottery_pool_config/index'
|
||||||
|
import {
|
||||||
|
lotteryPoolLabelById,
|
||||||
|
lotteryPoolOptionLabel,
|
||||||
|
type LotteryPoolOption
|
||||||
|
} from '@/views/plugin/dice/utils/lotteryPoolDisplay'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
|
|
||||||
@@ -242,6 +253,9 @@
|
|||||||
over_play_count?: number
|
over_play_count?: number
|
||||||
chain_free_mode?: number | boolean | string
|
chain_free_mode?: number | boolean | string
|
||||||
paid_planned_spins?: number
|
paid_planned_spins?: number
|
||||||
|
ante?: number
|
||||||
|
kill_mode_enabled?: number
|
||||||
|
test_safety_line?: number
|
||||||
create_time?: string
|
create_time?: string
|
||||||
admin_id?: number | null
|
admin_id?: number | null
|
||||||
admin_name?: string
|
admin_name?: string
|
||||||
@@ -278,6 +292,35 @@
|
|||||||
return t('page.table.chainModeNo')
|
return t('page.table.chainModeNo')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatTestSafetyLineDetail(record: RecordRow | null): string {
|
||||||
|
if (!record) return t('page.detail.dash')
|
||||||
|
if (Number(record.kill_mode_enabled ?? 0) !== 1) {
|
||||||
|
return t('page.detail.killModeOff')
|
||||||
|
}
|
||||||
|
const line = record.test_safety_line
|
||||||
|
if (line === null || line === undefined) {
|
||||||
|
return t('page.detail.dash')
|
||||||
|
}
|
||||||
|
return String(line)
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatAnteDetail(record: RecordRow | null): string {
|
||||||
|
if (!record) return t('page.detail.dash')
|
||||||
|
const snap = record.tier_weights_snapshot
|
||||||
|
const isRandom =
|
||||||
|
snap &&
|
||||||
|
typeof snap === 'object' &&
|
||||||
|
(snap as { ante_random?: boolean }).ante_random === true
|
||||||
|
if (isRandom) {
|
||||||
|
return t('page.table.anteRandom')
|
||||||
|
}
|
||||||
|
const ante = record.ante
|
||||||
|
if (ante === null || ante === undefined) {
|
||||||
|
return t('page.detail.dash')
|
||||||
|
}
|
||||||
|
return String(ante)
|
||||||
|
}
|
||||||
|
|
||||||
function formatTestCountDisplay(record: RecordRow | null): string {
|
function formatTestCountDisplay(record: RecordRow | null): string {
|
||||||
if (!record) return t('page.detail.dash')
|
if (!record) return t('page.detail.dash')
|
||||||
const status = Number(record.status)
|
const status = Number(record.status)
|
||||||
@@ -316,7 +359,18 @@
|
|||||||
const importing = ref(false)
|
const importing = ref(false)
|
||||||
const importPaidLotteryConfigId = ref<number | null>(null)
|
const importPaidLotteryConfigId = ref<number | null>(null)
|
||||||
const importFreeLotteryConfigId = ref<number | null>(null)
|
const importFreeLotteryConfigId = ref<number | null>(null)
|
||||||
const lotteryConfigOptions = ref<Array<{ id: number; name: string }>>([])
|
const lotteryConfigOptions = ref<LotteryPoolOption[]>([])
|
||||||
|
|
||||||
|
function formatRecordPaidPoolName(record: RecordRow | null): string {
|
||||||
|
if (!record) return t('page.detail.dash')
|
||||||
|
const id = record.paid_lottery_config_id ?? record.lottery_config_id ?? null
|
||||||
|
return lotteryPoolLabelById(id, lotteryConfigOptions.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatRecordFreePoolName(record: RecordRow | null): string {
|
||||||
|
if (!record) return t('page.detail.dash')
|
||||||
|
return lotteryPoolLabelById(record.free_lottery_config_id ?? null, lotteryConfigOptions.value)
|
||||||
|
}
|
||||||
|
|
||||||
function tierWeightsToTableData(weightsMap: Record<string, number> | null | undefined) {
|
function tierWeightsToTableData(weightsMap: Record<string, number> | null | undefined) {
|
||||||
const dash = t('page.detail.dash')
|
const dash = t('page.detail.dash')
|
||||||
@@ -496,6 +550,15 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.modelValue,
|
||||||
|
(open) => {
|
||||||
|
if (open) {
|
||||||
|
void loadLotteryOptions()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
function openImport() {
|
function openImport() {
|
||||||
importPaidLotteryConfigId.value =
|
importPaidLotteryConfigId.value =
|
||||||
props.record?.paid_lottery_config_id ?? props.record?.lottery_config_id ?? null
|
props.record?.paid_lottery_config_id ?? props.record?.lottery_config_id ?? null
|
||||||
|
|||||||
@@ -1,36 +1,31 @@
|
|||||||
<template>
|
<template>
|
||||||
<el-dialog
|
<el-dialog
|
||||||
v-model="visible"
|
v-model="visible"
|
||||||
:title="dialogType === 'add' ? $t('page.form.titleAdd') : $t('page.form.titleEdit')"
|
:title="$t('page.form.titleEdit')"
|
||||||
width="600px"
|
width="600px"
|
||||||
align-center
|
align-center
|
||||||
:close-on-click-modal="false"
|
:close-on-click-modal="false"
|
||||||
@close="handleClose"
|
@close="handleClose"
|
||||||
>
|
>
|
||||||
<el-form ref="formRef" :model="formData" :rules="rules" label-width="120px">
|
<el-form ref="formRef" :model="formData" label-width="120px">
|
||||||
<el-form-item :label="$t('page.form.labelTestCount')" prop="test_count">
|
<el-form-item :label="$t('page.form.labelTestCount')" prop="test_count">
|
||||||
<el-input v-model="formData.test_count" :placeholder="$t('page.form.placeholderTestCount')" />
|
<el-input v-model="formData.test_count" :placeholder="$t('page.form.placeholderTestCount')" disabled />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item :label="$t('page.form.labelWeightSnapshot')" prop="weight_config_snapshot">
|
<el-form-item :label="$t('page.form.labelWeightSnapshot')" prop="weight_config_snapshot">
|
||||||
<el-input v-model="formData.weight_config_snapshot" :placeholder="$t('page.form.placeholderWeightSnapshot')" />
|
<el-input v-model="formData.weight_config_snapshot" :placeholder="$t('page.form.placeholderWeightSnapshot')" disabled />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item :label="$t('page.form.labelResultCounts')" prop="result_counts">
|
<el-form-item :label="$t('page.form.labelResultCounts')" prop="result_counts">
|
||||||
<el-input v-model="formData.result_counts" :placeholder="$t('page.form.placeholderResultCounts')" />
|
<el-input v-model="formData.result_counts" :placeholder="$t('page.form.placeholderResultCounts')" disabled />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<el-button @click="handleClose">{{ $t('common.cancel') }}</el-button>
|
<el-button @click="handleClose">{{ $t('form.close') }}</el-button>
|
||||||
<el-button type="primary" @click="handleSubmit">{{ $t('table.form.submit') }}</el-button>
|
|
||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import api from '../../../api/reward_config_record/index'
|
import type { FormInstance } from 'element-plus'
|
||||||
import { ElMessage } from 'element-plus'
|
|
||||||
import type { FormInstance, FormRules } from 'element-plus'
|
|
||||||
import { useI18n } from 'vue-i18n'
|
|
||||||
import { withChannelDeptParams } from '@/composables/useChannelDeptScope'
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
modelValue: boolean
|
modelValue: boolean
|
||||||
@@ -45,12 +40,11 @@
|
|||||||
|
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
modelValue: false,
|
modelValue: false,
|
||||||
dialogType: 'add',
|
dialogType: 'edit',
|
||||||
data: undefined
|
data: undefined
|
||||||
})
|
})
|
||||||
|
|
||||||
const emit = defineEmits<Emits>()
|
const emit = defineEmits<Emits>()
|
||||||
const { t } = useI18n()
|
|
||||||
|
|
||||||
const formRef = ref<FormInstance>()
|
const formRef = ref<FormInstance>()
|
||||||
|
|
||||||
@@ -62,13 +56,6 @@
|
|||||||
set: (value) => emit('update:modelValue', value)
|
set: (value) => emit('update:modelValue', value)
|
||||||
})
|
})
|
||||||
|
|
||||||
/**
|
|
||||||
* 表单验证规则
|
|
||||||
*/
|
|
||||||
const rules = computed<FormRules>(() => ({
|
|
||||||
test_count: [{ required: true, message: t('page.form.ruleTestCountRequired'), trigger: 'blur' }]
|
|
||||||
}))
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 初始数据
|
* 初始数据
|
||||||
*/
|
*/
|
||||||
@@ -76,7 +63,7 @@
|
|||||||
id: null,
|
id: null,
|
||||||
test_count: 100,
|
test_count: 100,
|
||||||
weight_config_snapshot: '',
|
weight_config_snapshot: '',
|
||||||
result_counts: '',
|
result_counts: ''
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -85,7 +72,7 @@
|
|||||||
const formData = reactive({ ...initialFormData })
|
const formData = reactive({ ...initialFormData })
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 监听弹窗打开,初始化表单数据
|
* 监听弹窗打开,初始化表单数据(仅查看)
|
||||||
*/
|
*/
|
||||||
watch(
|
watch(
|
||||||
() => props.modelValue,
|
() => props.modelValue,
|
||||||
@@ -100,9 +87,7 @@
|
|||||||
* 初始化页面数据
|
* 初始化页面数据
|
||||||
*/
|
*/
|
||||||
const initPage = async () => {
|
const initPage = async () => {
|
||||||
// 先重置为初始值
|
|
||||||
Object.assign(formData, initialFormData)
|
Object.assign(formData, initialFormData)
|
||||||
// 如果有数据,则填充数据
|
|
||||||
if (props.data) {
|
if (props.data) {
|
||||||
await nextTick()
|
await nextTick()
|
||||||
initForm()
|
initForm()
|
||||||
@@ -110,7 +95,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 初始化表单数据
|
* 回填表单数据
|
||||||
*/
|
*/
|
||||||
const initForm = () => {
|
const initForm = () => {
|
||||||
if (props.data) {
|
if (props.data) {
|
||||||
@@ -123,31 +108,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 关闭弹窗并重置表单
|
* 关闭弹窗
|
||||||
*/
|
*/
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
visible.value = false
|
visible.value = false
|
||||||
formRef.value?.resetFields()
|
formRef.value?.resetFields()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 提交表单
|
|
||||||
*/
|
|
||||||
const handleSubmit = async () => {
|
|
||||||
if (!formRef.value) return
|
|
||||||
try {
|
|
||||||
await formRef.value.validate()
|
|
||||||
if (props.dialogType === 'add') {
|
|
||||||
await api.save(withChannelDeptParams(formData))
|
|
||||||
ElMessage.success(t('page.form.addSuccess'))
|
|
||||||
} else {
|
|
||||||
await api.update(withChannelDeptParams(formData))
|
|
||||||
ElMessage.success(t('page.form.editSuccess'))
|
|
||||||
}
|
|
||||||
emit('success')
|
|
||||||
handleClose()
|
|
||||||
} catch (error) {
|
|
||||||
console.log('表单验证失败:', error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -0,0 +1,65 @@
|
|||||||
|
import { router } from '@/router'
|
||||||
|
import type { DatePresetKey } from './dateRangePresets'
|
||||||
|
|
||||||
|
export const WALLET_RECORD_PATH = '/dice/player_wallet_record/index'
|
||||||
|
export const PLAY_RECORD_PATH = '/dice/play_record/index'
|
||||||
|
export const PLAYER_PATH = '/dice/player/index'
|
||||||
|
|
||||||
|
export interface WalletRecordNavOptions {
|
||||||
|
type?: number
|
||||||
|
date?: string | null
|
||||||
|
datePreset?: DatePresetKey
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PlayRecordNavOptions {
|
||||||
|
date?: string | null
|
||||||
|
datePreset?: DatePresetKey
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildDateQuery(opts: { date?: string | null; datePreset?: DatePresetKey }) {
|
||||||
|
const query: Record<string, string> = {}
|
||||||
|
if (opts.date) {
|
||||||
|
query.date = opts.date
|
||||||
|
} else if (opts.datePreset) {
|
||||||
|
query.datePreset = opts.datePreset
|
||||||
|
}
|
||||||
|
return query
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildWalletRecordRouteQuery(opts: WalletRecordNavOptions): Record<string, string> {
|
||||||
|
const query = buildDateQuery(opts)
|
||||||
|
if (opts.type !== undefined && opts.type !== null) {
|
||||||
|
query.type = String(opts.type)
|
||||||
|
}
|
||||||
|
return query
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildPlayRecordRouteQuery(opts: PlayRecordNavOptions): Record<string, string> {
|
||||||
|
return buildDateQuery(opts)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function openWalletRecord(opts: WalletRecordNavOptions) {
|
||||||
|
const query = buildWalletRecordRouteQuery(opts)
|
||||||
|
void router.push({ path: WALLET_RECORD_PATH, query }).catch(() => undefined)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function openPlayRecord(opts: PlayRecordNavOptions) {
|
||||||
|
const query = buildPlayRecordRouteQuery(opts)
|
||||||
|
void router.push({ path: PLAY_RECORD_PATH, query }).catch(() => undefined)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function openPlayerList(opts: PlayRecordNavOptions) {
|
||||||
|
const query = buildPlayRecordRouteQuery(opts)
|
||||||
|
void router.push({ path: PLAYER_PATH, query }).catch(() => undefined)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 工作台当前统计日期转跳转参数:有选中日用 date,清空周统计时用近7天 */
|
||||||
|
export function dashboardDateNavParams(selectedDate: string | null): {
|
||||||
|
date?: string
|
||||||
|
datePreset?: DatePresetKey
|
||||||
|
} {
|
||||||
|
if (selectedDate) {
|
||||||
|
return { date: selectedDate }
|
||||||
|
}
|
||||||
|
return { datePreset: 'last7days' }
|
||||||
|
}
|
||||||
101
saiadmin-artd/src/views/plugin/dice/utils/dateRangePresets.ts
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
import type { LocationQuery } from 'vue-router'
|
||||||
|
|
||||||
|
export type DatePresetKey = 'today' | 'yesterday' | 'last7days'
|
||||||
|
|
||||||
|
function pad2(n: number): string {
|
||||||
|
return String(n).padStart(2, '0')
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatDateYmd(date: Date): string {
|
||||||
|
return `${date.getFullYear()}-${pad2(date.getMonth() + 1)}-${pad2(date.getDate())}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function startOfDay(date: Date): Date {
|
||||||
|
return new Date(date.getFullYear(), date.getMonth(), date.getDate(), 0, 0, 0, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
function endOfDay(date: Date): Date {
|
||||||
|
return new Date(date.getFullYear(), date.getMonth(), date.getDate(), 23, 59, 59, 999)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatDateTime(date: Date): string {
|
||||||
|
return `${formatDateYmd(date)} ${pad2(date.getHours())}:${pad2(date.getMinutes())}:${pad2(date.getSeconds())}`
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getCreateTimeRangeByPreset(preset: DatePresetKey): [string, string] {
|
||||||
|
const now = new Date()
|
||||||
|
if (preset === 'today') {
|
||||||
|
return [formatDateTime(startOfDay(now)), formatDateTime(endOfDay(now))]
|
||||||
|
}
|
||||||
|
if (preset === 'yesterday') {
|
||||||
|
const day = new Date(now)
|
||||||
|
day.setDate(day.getDate() - 1)
|
||||||
|
return [formatDateTime(startOfDay(day)), formatDateTime(endOfDay(day))]
|
||||||
|
}
|
||||||
|
const start = new Date(now)
|
||||||
|
start.setDate(start.getDate() - 6)
|
||||||
|
return [formatDateTime(startOfDay(start)), formatDateTime(endOfDay(now))]
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getCreateTimeRangeByDate(ymd: string): [string, string] | undefined {
|
||||||
|
if (!/^\d{4}-\d{2}-\d{2}$/.test(ymd)) return undefined
|
||||||
|
const parts = ymd.split('-').map((v) => Number(v))
|
||||||
|
const date = new Date(parts[0], parts[1] - 1, parts[2])
|
||||||
|
if (Number.isNaN(date.getTime())) return undefined
|
||||||
|
return [formatDateTime(startOfDay(date)), formatDateTime(endOfDay(date))]
|
||||||
|
}
|
||||||
|
|
||||||
|
export function detectPresetFromRange(range?: [string, string] | null): DatePresetKey | null {
|
||||||
|
if (!range || range.length !== 2) return null
|
||||||
|
const presets: DatePresetKey[] = ['today', 'yesterday', 'last7days']
|
||||||
|
for (const preset of presets) {
|
||||||
|
const expected = getCreateTimeRangeByPreset(preset)
|
||||||
|
if (expected[0] === range[0] && expected[1] === range[1]) {
|
||||||
|
return preset
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
function readQueryString(query: LocationQuery, key: string): string {
|
||||||
|
const raw = query[key]
|
||||||
|
if (Array.isArray(raw)) return raw[0] ? String(raw[0]) : ''
|
||||||
|
return raw != null ? String(raw) : ''
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RecordRouteFilter {
|
||||||
|
create_time?: [string, string]
|
||||||
|
type?: number
|
||||||
|
datePreset?: DatePresetKey
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseRecordRouteQuery(query: LocationQuery): RecordRouteFilter {
|
||||||
|
const result: RecordRouteFilter = {}
|
||||||
|
const date = readQueryString(query, 'date')
|
||||||
|
const datePreset = readQueryString(query, 'datePreset') as DatePresetKey
|
||||||
|
|
||||||
|
if (date) {
|
||||||
|
const range = getCreateTimeRangeByDate(date)
|
||||||
|
if (range) {
|
||||||
|
result.create_time = range
|
||||||
|
}
|
||||||
|
} else if (datePreset === 'today' || datePreset === 'yesterday' || datePreset === 'last7days') {
|
||||||
|
result.datePreset = datePreset
|
||||||
|
result.create_time = getCreateTimeRangeByPreset(datePreset)
|
||||||
|
}
|
||||||
|
|
||||||
|
const typeRaw = readQueryString(query, 'type')
|
||||||
|
if (typeRaw !== '' && typeRaw !== undefined) {
|
||||||
|
const typeNum = Number(typeRaw)
|
||||||
|
if (Number.isFinite(typeNum)) {
|
||||||
|
result.type = typeNum
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
export function hasRecordRouteFilter(query: LocationQuery): boolean {
|
||||||
|
const filter = parseRecordRouteQuery(query)
|
||||||
|
return filter.create_time != null || filter.type !== undefined
|
||||||
|
}
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
/** 彩金池选项/关联行通用结构 */
|
||||||
|
export interface LotteryPoolOption {
|
||||||
|
id: number
|
||||||
|
name?: string
|
||||||
|
remark?: string
|
||||||
|
display_name?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 彩金池后台展示名(奖池名称):优先 display_name / remark,不用内部 name 作为首选 */
|
||||||
|
export function lotteryPoolDisplayLabel(item?: LotteryPoolOption | null): string {
|
||||||
|
if (!item) {
|
||||||
|
return '-'
|
||||||
|
}
|
||||||
|
const display = String(item.display_name ?? '').trim()
|
||||||
|
if (display) {
|
||||||
|
return display
|
||||||
|
}
|
||||||
|
const remark = String(item.remark ?? '').trim()
|
||||||
|
if (remark) {
|
||||||
|
return remark
|
||||||
|
}
|
||||||
|
return String(item.name ?? '').trim() || '-'
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 下拉选项文案:默认仅奖池名称;withId=true 时附带 ID */
|
||||||
|
export function lotteryPoolOptionLabel(
|
||||||
|
item: LotteryPoolOption,
|
||||||
|
options?: { withId?: boolean }
|
||||||
|
): string {
|
||||||
|
const label = lotteryPoolDisplayLabel(item)
|
||||||
|
if (options?.withId && item.id > 0) {
|
||||||
|
return label !== '-' ? `${label} (#${item.id})` : `#${item.id}`
|
||||||
|
}
|
||||||
|
return label !== '-' ? label : `#${item.id}`
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 列表行关联彩金池(含 diceLotteryPoolConfig 关联) */
|
||||||
|
export function lotteryPoolRowLabel(row?: {
|
||||||
|
diceLotteryPoolConfig?: LotteryPoolOption | null
|
||||||
|
lottery_config_id?: number | null | string
|
||||||
|
} | null): string {
|
||||||
|
if (!row) {
|
||||||
|
return '-'
|
||||||
|
}
|
||||||
|
const pool = row.diceLotteryPoolConfig
|
||||||
|
if (pool && (pool.id || pool.remark || pool.name || pool.display_name)) {
|
||||||
|
return lotteryPoolDisplayLabel(pool)
|
||||||
|
}
|
||||||
|
const id = row.lottery_config_id
|
||||||
|
if (id !== null && id !== undefined && id !== '') {
|
||||||
|
return `#${id}`
|
||||||
|
}
|
||||||
|
return '-'
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 规范化接口返回的彩金池选项 */
|
||||||
|
export function normalizeLotteryPoolOption(raw: Record<string, unknown>): LotteryPoolOption {
|
||||||
|
const id = Number(raw.id ?? 0)
|
||||||
|
const name = String(raw.name ?? '')
|
||||||
|
const remark = String(raw.remark ?? '')
|
||||||
|
const displayName = String(raw.display_name ?? '').trim()
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
remark,
|
||||||
|
display_name: displayName !== '' ? displayName : remark !== '' ? remark : name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 按奖池名称 / 内部标识 / ID 过滤下拉 */
|
||||||
|
export function filterLotteryPoolOptionsByQuery(
|
||||||
|
list: LotteryPoolOption[],
|
||||||
|
query: string
|
||||||
|
): LotteryPoolOption[] {
|
||||||
|
const q = (query || '').trim().toLowerCase()
|
||||||
|
if (!q) {
|
||||||
|
return [...list]
|
||||||
|
}
|
||||||
|
return list.filter((item) => {
|
||||||
|
const label = lotteryPoolDisplayLabel(item).toLowerCase()
|
||||||
|
const code = String(item.name ?? '').toLowerCase()
|
||||||
|
return label.includes(q) || code.includes(q) || String(item.id).includes(q)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 根据 ID 从选项列表解析奖池名称 */
|
||||||
|
export function lotteryPoolLabelById(
|
||||||
|
poolId: number | null | undefined,
|
||||||
|
options: LotteryPoolOption[]
|
||||||
|
): string {
|
||||||
|
if (poolId == null || poolId <= 0) {
|
||||||
|
return '-'
|
||||||
|
}
|
||||||
|
const found = options.find((o) => o.id === poolId)
|
||||||
|
if (found) {
|
||||||
|
return lotteryPoolDisplayLabel(found)
|
||||||
|
}
|
||||||
|
return `#${poolId}`
|
||||||
|
}
|
||||||
549
saiadmin-artd/src/views/system/admin_guide/index.vue
Normal file
@@ -0,0 +1,549 @@
|
|||||||
|
<template>
|
||||||
|
<div class="art-full-height admin-guide-page">
|
||||||
|
<ElCard class="art-card-xs flex flex-col h-full mt-0" shadow="never">
|
||||||
|
<template #header>
|
||||||
|
<div class="flex flex-wrap items-center justify-between gap-3">
|
||||||
|
<div>
|
||||||
|
<b>{{ $t('page.title') }}</b>
|
||||||
|
<div v-if="meta.filePath" class="mt-1 text-xs text-g-500">
|
||||||
|
{{ $t('page.meta.filePath') }}:{{ meta.filePath }}
|
||||||
|
<span v-if="meta.updateTime" class="ml-3">
|
||||||
|
{{ $t('page.meta.updateTime') }}:{{ meta.updateTime }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ElSpace wrap>
|
||||||
|
<ElButton
|
||||||
|
v-permission="'system:admin_guide:index:read'"
|
||||||
|
:loading="loading"
|
||||||
|
@click="loadContent"
|
||||||
|
>
|
||||||
|
<template #icon>
|
||||||
|
<ArtSvgIcon icon="ri:refresh-line" />
|
||||||
|
</template>
|
||||||
|
{{ $t('page.toolbar.refresh') }}
|
||||||
|
</ElButton>
|
||||||
|
<ElButton
|
||||||
|
v-if="!isEditing"
|
||||||
|
v-permission="'system:admin_guide:index:edit'"
|
||||||
|
type="primary"
|
||||||
|
@click="startEdit"
|
||||||
|
>
|
||||||
|
<template #icon>
|
||||||
|
<ArtSvgIcon icon="ri:pencil-line" />
|
||||||
|
</template>
|
||||||
|
{{ $t('page.toolbar.edit') }}
|
||||||
|
</ElButton>
|
||||||
|
<template v-if="isEditing">
|
||||||
|
<ElButton @click="handleCancel">
|
||||||
|
{{ $t('page.toolbar.cancel') }}
|
||||||
|
</ElButton>
|
||||||
|
<ElButton
|
||||||
|
v-permission="'system:admin_guide:index:save'"
|
||||||
|
type="primary"
|
||||||
|
:loading="saving"
|
||||||
|
@click="handleSave"
|
||||||
|
>
|
||||||
|
<template #icon>
|
||||||
|
<ArtSvgIcon icon="ri:save-line" />
|
||||||
|
</template>
|
||||||
|
{{ $t('page.toolbar.save') }}
|
||||||
|
</ElButton>
|
||||||
|
</template>
|
||||||
|
</ElSpace>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div v-loading="loading" class="admin-guide-body flex-1 min-h-0">
|
||||||
|
<SaMdEditor
|
||||||
|
v-if="isEditing"
|
||||||
|
v-model="editContent"
|
||||||
|
class="admin-guide-editor"
|
||||||
|
height="calc(100vh - 220px)"
|
||||||
|
min-height="480px"
|
||||||
|
/>
|
||||||
|
<div v-else class="admin-guide-read flex h-full min-h-0">
|
||||||
|
<aside class="admin-guide-catalog flex-shrink-0">
|
||||||
|
<div class="catalog-title">{{ $t('page.catalog.title') }}</div>
|
||||||
|
<ElScrollbar class="catalog-scroll">
|
||||||
|
<nav v-if="tocList.length" class="guide-toc">
|
||||||
|
<button
|
||||||
|
v-for="(item, index) in tocList"
|
||||||
|
:key="`${item.level}-${item.text}-${index}`"
|
||||||
|
type="button"
|
||||||
|
class="guide-toc-item"
|
||||||
|
:class="[
|
||||||
|
`guide-toc-level-${item.level}`,
|
||||||
|
{ 'is-active': activeTocIndex === index }
|
||||||
|
]"
|
||||||
|
:title="item.text"
|
||||||
|
@click="scrollToHeading(item, index)"
|
||||||
|
>
|
||||||
|
{{ item.text }}
|
||||||
|
</button>
|
||||||
|
</nav>
|
||||||
|
<div v-else class="guide-toc-empty">{{ $t('page.catalog.empty') }}</div>
|
||||||
|
</ElScrollbar>
|
||||||
|
</aside>
|
||||||
|
<div
|
||||||
|
id="admin-guide-scroll"
|
||||||
|
ref="previewScrollRef"
|
||||||
|
class="admin-guide-preview-wrap flex-1 min-h-0 overflow-auto"
|
||||||
|
>
|
||||||
|
<MdPreview
|
||||||
|
:editor-id="previewEditorId"
|
||||||
|
:model-value="previewContent"
|
||||||
|
:theme="previewTheme"
|
||||||
|
preview-theme="github"
|
||||||
|
no-img-zoom-in
|
||||||
|
class="admin-guide-preview"
|
||||||
|
@on-html-changed="handlePreviewHtmlReady"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ElCard>
|
||||||
|
|
||||||
|
<ElImageViewer
|
||||||
|
v-if="imageViewerVisible"
|
||||||
|
:key="imageViewerIndex"
|
||||||
|
:url-list="previewImageUrls"
|
||||||
|
:initial-index="imageViewerIndex"
|
||||||
|
:hide-on-click-modal="true"
|
||||||
|
:z-index="3000"
|
||||||
|
teleported
|
||||||
|
@close="closeImageViewer"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import { ElImageViewer, ElMessageBox } from 'element-plus'
|
||||||
|
import { MdPreview } from 'md-editor-v3'
|
||||||
|
import 'md-editor-v3/lib/preview.css'
|
||||||
|
import SaMdEditor from '@/components/sai/sa-md-editor/index.vue'
|
||||||
|
import { useSettingStore } from '@/store/modules/setting'
|
||||||
|
import api from '@/api/system/admin_guide'
|
||||||
|
|
||||||
|
defineOptions({ name: 'SystemAdminGuide' })
|
||||||
|
|
||||||
|
interface GuideTocItem {
|
||||||
|
level: number
|
||||||
|
text: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const { t } = useI18n()
|
||||||
|
const settingStore = useSettingStore()
|
||||||
|
|
||||||
|
const previewEditorId = 'admin-guide-preview'
|
||||||
|
const previewScrollRef = ref<HTMLElement | null>(null)
|
||||||
|
const loading = ref(false)
|
||||||
|
const saving = ref(false)
|
||||||
|
const isEditing = ref(false)
|
||||||
|
const previewContent = ref('')
|
||||||
|
const editContent = ref('')
|
||||||
|
const originalContent = ref('')
|
||||||
|
const meta = ref<{ filePath?: string; updateTime?: string }>({})
|
||||||
|
const tocList = ref<GuideTocItem[]>([])
|
||||||
|
const activeTocIndex = ref(-1)
|
||||||
|
const previewImageUrls = ref<string[]>([])
|
||||||
|
const imageViewerVisible = ref(false)
|
||||||
|
const imageViewerIndex = ref(0)
|
||||||
|
|
||||||
|
let previewImageClickHandler: ((event: Event) => void) | null = null
|
||||||
|
let previewEnhanceTimer: ReturnType<typeof setTimeout> | null = null
|
||||||
|
|
||||||
|
const previewTheme = computed(() => (settingStore.isDark ? 'dark' : 'light'))
|
||||||
|
|
||||||
|
const getStaticFileBase = (): string => {
|
||||||
|
const apiUrl = import.meta.env.VITE_API_URL || ''
|
||||||
|
if (apiUrl.startsWith('http')) {
|
||||||
|
return apiUrl.replace(/\/$/, '')
|
||||||
|
}
|
||||||
|
const proxyUrl = import.meta.env.VITE_API_PROXY_URL || ''
|
||||||
|
if (proxyUrl) {
|
||||||
|
return String(proxyUrl).replace(/\/$/, '')
|
||||||
|
}
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
const resolveGuideImages = (content: string): string => {
|
||||||
|
const staticBase = getStaticFileBase()
|
||||||
|
if (!staticBase) {
|
||||||
|
return content
|
||||||
|
}
|
||||||
|
return content.replace(/!\[([^\]]*)\]\((\/docs\/picture\/[^)]+)\)/g, (_match, alt, path) => {
|
||||||
|
return ``
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const parseTocFromMarkdown = (content: string): GuideTocItem[] => {
|
||||||
|
const items: GuideTocItem[] = []
|
||||||
|
for (const line of content.split('\n')) {
|
||||||
|
const match = line.match(/^(#{1,6})\s+(.+)$/)
|
||||||
|
if (!match) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
items.push({
|
||||||
|
level: match[1].length,
|
||||||
|
text: match[2].trim()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return items
|
||||||
|
}
|
||||||
|
|
||||||
|
const refreshTocList = () => {
|
||||||
|
tocList.value = parseTocFromMarkdown(originalContent.value)
|
||||||
|
activeTocIndex.value = -1
|
||||||
|
}
|
||||||
|
|
||||||
|
const collectPreviewImages = () => {
|
||||||
|
const container = previewScrollRef.value
|
||||||
|
if (!container) {
|
||||||
|
previewImageUrls.value = []
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const imgs = container.querySelectorAll('img')
|
||||||
|
previewImageUrls.value = Array.from(imgs).map((img) => {
|
||||||
|
const el = img as HTMLImageElement
|
||||||
|
return el.currentSrc || el.src
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const findImageIndex = (img: HTMLImageElement): number => {
|
||||||
|
collectPreviewImages()
|
||||||
|
const targetSrc = img.currentSrc || img.src
|
||||||
|
let index = previewImageUrls.value.indexOf(targetSrc)
|
||||||
|
if (index >= 0) {
|
||||||
|
return index
|
||||||
|
}
|
||||||
|
index = previewImageUrls.value.findIndex((url) => targetSrc.endsWith(url) || url.endsWith(targetSrc))
|
||||||
|
if (index >= 0) {
|
||||||
|
return index
|
||||||
|
}
|
||||||
|
previewImageUrls.value = [...previewImageUrls.value, targetSrc]
|
||||||
|
return previewImageUrls.value.length - 1
|
||||||
|
}
|
||||||
|
|
||||||
|
const openImageViewerByImg = (img: HTMLImageElement) => {
|
||||||
|
const index = findImageIndex(img)
|
||||||
|
openImageViewer(index)
|
||||||
|
}
|
||||||
|
|
||||||
|
const openImageViewer = (index: number) => {
|
||||||
|
if (index < 0 || index >= previewImageUrls.value.length) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
imageViewerIndex.value = index
|
||||||
|
imageViewerVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const closeImageViewer = () => {
|
||||||
|
imageViewerVisible.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
const unbindPreviewImageClick = (clearTimer = true) => {
|
||||||
|
const container = previewScrollRef.value
|
||||||
|
if (container && previewImageClickHandler) {
|
||||||
|
container.removeEventListener('click', previewImageClickHandler, true)
|
||||||
|
}
|
||||||
|
previewImageClickHandler = null
|
||||||
|
if (clearTimer && previewEnhanceTimer) {
|
||||||
|
clearTimeout(previewEnhanceTimer)
|
||||||
|
previewEnhanceTimer = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const bindPreviewImageClick = () => {
|
||||||
|
unbindPreviewImageClick(false)
|
||||||
|
const container = previewScrollRef.value
|
||||||
|
if (!container) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
collectPreviewImages()
|
||||||
|
previewImageClickHandler = (event: Event) => {
|
||||||
|
const imgEl = (event.target as Element | null)?.closest('img')
|
||||||
|
if (!(imgEl instanceof HTMLImageElement)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!container.contains(imgEl)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
event.preventDefault()
|
||||||
|
event.stopPropagation()
|
||||||
|
openImageViewerByImg(imgEl)
|
||||||
|
}
|
||||||
|
container.addEventListener('click', previewImageClickHandler, true)
|
||||||
|
|
||||||
|
const imgs = container.querySelectorAll('img')
|
||||||
|
imgs.forEach((img) => {
|
||||||
|
const el = img as HTMLImageElement
|
||||||
|
el.style.cursor = 'zoom-in'
|
||||||
|
el.title = t('page.image.zoom')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const handlePreviewHtmlReady = () => {
|
||||||
|
if (previewEnhanceTimer) {
|
||||||
|
clearTimeout(previewEnhanceTimer)
|
||||||
|
}
|
||||||
|
previewEnhanceTimer = setTimeout(() => {
|
||||||
|
bindPreviewImageClick()
|
||||||
|
previewEnhanceTimer = null
|
||||||
|
}, 50)
|
||||||
|
}
|
||||||
|
|
||||||
|
const setupPreviewEnhancements = async () => {
|
||||||
|
await nextTick()
|
||||||
|
refreshTocList()
|
||||||
|
handlePreviewHtmlReady()
|
||||||
|
}
|
||||||
|
|
||||||
|
const scrollToHeading = (item: GuideTocItem, index: number) => {
|
||||||
|
const container = previewScrollRef.value
|
||||||
|
if (!container) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const headings = container.querySelectorAll('h1, h2, h3, h4, h5, h6')
|
||||||
|
for (const heading of Array.from(headings)) {
|
||||||
|
if (heading.textContent?.trim() === item.text) {
|
||||||
|
activeTocIndex.value = index
|
||||||
|
heading.scrollIntoView({ behavior: 'smooth', block: 'start' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadContent = async () => {
|
||||||
|
loading.value = true
|
||||||
|
unbindPreviewImageClick()
|
||||||
|
try {
|
||||||
|
const res = await api.read()
|
||||||
|
const data = res as { content?: string; file_path?: string; update_time?: string }
|
||||||
|
const rawContent = data.content ?? ''
|
||||||
|
editContent.value = rawContent
|
||||||
|
originalContent.value = rawContent
|
||||||
|
previewContent.value = resolveGuideImages(rawContent)
|
||||||
|
meta.value = {
|
||||||
|
filePath: data.file_path,
|
||||||
|
updateTime: data.update_time
|
||||||
|
}
|
||||||
|
await setupPreviewEnhancements()
|
||||||
|
} catch {
|
||||||
|
// 错误由 http 工具统一处理
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const startEdit = () => {
|
||||||
|
editContent.value = originalContent.value
|
||||||
|
isEditing.value = true
|
||||||
|
unbindPreviewImageClick()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleCancel = async () => {
|
||||||
|
if (editContent.value !== originalContent.value) {
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm(t('page.message.cancelConfirm'), {
|
||||||
|
type: 'warning'
|
||||||
|
})
|
||||||
|
} catch {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
editContent.value = originalContent.value
|
||||||
|
isEditing.value = false
|
||||||
|
await setupPreviewEnhancements()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSave = async () => {
|
||||||
|
if (!isEditing.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
saving.value = true
|
||||||
|
try {
|
||||||
|
const res = await api.save({ content: editContent.value })
|
||||||
|
const data = res as { content?: string; file_path?: string; update_time?: string }
|
||||||
|
const savedContent = data.content ?? editContent.value
|
||||||
|
editContent.value = savedContent
|
||||||
|
originalContent.value = savedContent
|
||||||
|
previewContent.value = resolveGuideImages(savedContent)
|
||||||
|
meta.value = {
|
||||||
|
filePath: data.file_path ?? meta.value.filePath,
|
||||||
|
updateTime: data.update_time ?? meta.value.updateTime
|
||||||
|
}
|
||||||
|
isEditing.value = false
|
||||||
|
await setupPreviewEnhancements()
|
||||||
|
} catch {
|
||||||
|
// 错误由 http 工具统一处理
|
||||||
|
} finally {
|
||||||
|
saving.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(previewContent, async () => {
|
||||||
|
if (!isEditing.value) {
|
||||||
|
await nextTick()
|
||||||
|
handlePreviewHtmlReady()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
loadContent()
|
||||||
|
})
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
unbindPreviewImageClick()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.admin-guide-page {
|
||||||
|
:deep(.el-card__body) {
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: 0;
|
||||||
|
padding-top: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-guide-body {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-guide-read {
|
||||||
|
min-height: 0;
|
||||||
|
gap: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-guide-catalog {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 240px;
|
||||||
|
min-height: 0;
|
||||||
|
padding: 4px 12px 12px 4px;
|
||||||
|
border-right: 1px solid var(--el-border-color-lighter);
|
||||||
|
}
|
||||||
|
|
||||||
|
.catalog-title {
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--el-text-color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.catalog-scroll {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.guide-toc {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
padding-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.guide-toc-item {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
padding: 5px 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.5;
|
||||||
|
color: var(--el-text-color-regular);
|
||||||
|
text-align: left;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
cursor: pointer;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: color 0.2s, background-color 0.2s;
|
||||||
|
|
||||||
|
&:hover,
|
||||||
|
&.is-active {
|
||||||
|
color: var(--el-color-primary);
|
||||||
|
background-color: var(--el-color-primary-light-9);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.guide-toc-level-1 {
|
||||||
|
padding-left: 8px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.guide-toc-level-2 {
|
||||||
|
padding-left: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.guide-toc-level-3 {
|
||||||
|
padding-left: 32px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.guide-toc-level-4 {
|
||||||
|
padding-left: 44px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.guide-toc-level-5,
|
||||||
|
.guide-toc-level-6 {
|
||||||
|
padding-left: 56px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.guide-toc-empty {
|
||||||
|
padding: 8px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-guide-preview-wrap {
|
||||||
|
position: relative;
|
||||||
|
padding: 8px 12px 8px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-guide-preview {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-guide-editor {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.admin-guide-read {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-guide-catalog {
|
||||||
|
width: 100%;
|
||||||
|
max-height: 200px;
|
||||||
|
padding-right: 0;
|
||||||
|
border-right: none;
|
||||||
|
border-bottom: 1px solid var(--el-border-color-lighter);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.admin-guide-preview-wrap .md-editor-preview-wrapper img,
|
||||||
|
.admin-guide-preview-wrap .md-editor-preview img,
|
||||||
|
.admin-guide-editor .md-editor-preview-wrapper img,
|
||||||
|
.admin-guide-editor .md-editor-preview img {
|
||||||
|
display: block;
|
||||||
|
width: 50% !important;
|
||||||
|
max-width: 50% !important;
|
||||||
|
height: auto !important;
|
||||||
|
margin: 8px 0;
|
||||||
|
cursor: zoom-in;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -106,8 +106,15 @@
|
|||||||
ElMessage.success('删除成功')
|
ElMessage.success('删除成功')
|
||||||
emit('success')
|
emit('success')
|
||||||
handleClose()
|
handleClose()
|
||||||
} catch (e: any) {
|
} catch (e: unknown) {
|
||||||
ElMessage.error(e?.message || '删除失败')
|
let msg = '删除失败'
|
||||||
|
if (e !== null && typeof e === 'object' && 'message' in e) {
|
||||||
|
const m = Reflect.get(e, 'message')
|
||||||
|
if (typeof m === 'string' && m !== '') {
|
||||||
|
msg = m
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ElMessage.error(msg)
|
||||||
} finally {
|
} finally {
|
||||||
submitting.value = false
|
submitting.value = false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -125,6 +125,10 @@
|
|||||||
} = useTable({
|
} = useTable({
|
||||||
core: {
|
core: {
|
||||||
apiFn: api.list,
|
apiFn: api.list,
|
||||||
|
apiParams: {
|
||||||
|
orderField: 'level',
|
||||||
|
orderType: 'desc'
|
||||||
|
},
|
||||||
columnsFactory: () => [
|
columnsFactory: () => [
|
||||||
{ prop: 'id', label: 'table.columns.common.no', minWidth: 60, align: 'center' },
|
{ prop: 'id', label: 'table.columns.common.no', minWidth: 60, align: 'center' },
|
||||||
{ prop: 'name', label: 'page.table.roleName', minWidth: 120 },
|
{ prop: 'name', label: 'page.table.roleName', minWidth: 120 },
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
# 数据库配置
|
# 数据库配置
|
||||||
|
|
||||||
DB_TYPE=mysql
|
DB_TYPE=mysql
|
||||||
DB_HOST=127.0.0.1
|
DB_HOST=127.0.0.1
|
||||||
DB_PORT=3306
|
DB_PORT=3306
|
||||||
DB_NAME=dafuweng-v3
|
DB_NAME=dafuweng-v3
|
||||||
DB_USER=dafuweng-v3
|
DB_USER=dafuweng-v3
|
||||||
DB_PASSWORD=tA6rciKLKxpFNGAm
|
DB_PASSWORD=123456
|
||||||
DB_PREFIX=
|
DB_PREFIX=
|
||||||
DB_POOL_MAX=32
|
DB_POOL_MAX=32
|
||||||
DB_POOL_MIN=4
|
DB_POOL_MIN=4
|
||||||
@@ -28,14 +29,16 @@ WEBMAN_CHANNEL_LISTEN_HOST=0.0.0.0
|
|||||||
GAME_URL=dice-v3-game.h55555game.top
|
GAME_URL=dice-v3-game.h55555game.top
|
||||||
|
|
||||||
# API 鉴权与用户(可选,不填则用默认值)
|
# API 鉴权与用户(可选,不填则用默认值)
|
||||||
|
# 平台对接 /api/v1/* 请求头 api-key(必填,与对接方约定)
|
||||||
|
API_KEY=
|
||||||
# authToken 签名密钥(必填,与客户端约定,用于 signature 校验)
|
# authToken 签名密钥(必填,与客户端约定,用于 signature 校验)
|
||||||
API_AUTH_TOKEN_SECRET=xF75oK91TQj13s0UmNIr1NBWMWGfflNO
|
API_AUTH_TOKEN_SECRET=
|
||||||
# authToken 时间戳允许误差秒数,防重放,默认 300
|
# authToken 时间戳允许误差秒数,防重放,默认 300
|
||||||
API_AUTH_TOKEN_TIME_TOLERANCE=300
|
API_AUTH_TOKEN_TIME_TOLERANCE=300
|
||||||
API_AUTH_TOKEN_EXP=86400
|
API_AUTH_TOKEN_EXP=86400
|
||||||
# API_USER_TOKEN_EXP=604800
|
# API_USER_TOKEN_EXP=604800
|
||||||
API_USER_CACHE_EXPIRE=86400
|
API_USER_CACHE_EXPIRE=86400
|
||||||
API_USER_ENCRYPT_KEY=Wj818SK8dhKBKNOY3PUTmZfhQDMCXEZi
|
API_USER_ENCRYPT_KEY=
|
||||||
|
|
||||||
# 验证码配置,支持cache|session
|
# 验证码配置,支持cache|session
|
||||||
CAPTCHA_MODE=cache
|
CAPTCHA_MODE=cache
|
||||||
|
|||||||
@@ -244,14 +244,19 @@ class GameController extends BaseController
|
|||||||
$timeoutRecord = null;
|
$timeoutRecord = null;
|
||||||
$timeout_message = '';
|
$timeout_message = '';
|
||||||
$adminId = null;
|
$adminId = null;
|
||||||
|
$timeoutDeptId = null;
|
||||||
try {
|
try {
|
||||||
$timeoutPlayer = DicePlayer::find($userId);
|
$timeoutPlayer = DicePlayer::find($userId);
|
||||||
$adminId = ($timeoutPlayer && ($timeoutPlayer->admin_id ?? null)) ? (int) $timeoutPlayer->admin_id : null;
|
$adminId = ($timeoutPlayer && ($timeoutPlayer->admin_id ?? null)) ? (int) $timeoutPlayer->admin_id : null;
|
||||||
|
if ($timeoutPlayer && isset($timeoutPlayer->dept_id) && $timeoutPlayer->dept_id !== null && $timeoutPlayer->dept_id !== '') {
|
||||||
|
$timeoutDeptId = (int) $timeoutPlayer->dept_id;
|
||||||
|
}
|
||||||
} catch (\Throwable $_) {
|
} catch (\Throwable $_) {
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
$timeoutRecord = DicePlayRecord::create([
|
$timeoutRecord = DicePlayRecord::create([
|
||||||
'player_id' => $userId,
|
'player_id' => $userId,
|
||||||
|
'dept_id' => $timeoutDeptId,
|
||||||
'admin_id' => $adminId,
|
'admin_id' => $adminId,
|
||||||
'lottery_config_id' => 0,
|
'lottery_config_id' => 0,
|
||||||
'lottery_type' => 0,
|
'lottery_type' => 0,
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ namespace app\api\controller\v1;
|
|||||||
use app\api\cache\AuthTokenCache;
|
use app\api\cache\AuthTokenCache;
|
||||||
use app\api\controller\BaseController;
|
use app\api\controller\BaseController;
|
||||||
use app\api\util\ReturnCode;
|
use app\api\util\ReturnCode;
|
||||||
|
use plugin\saiadmin\app\model\system\SystemUser;
|
||||||
use support\Request;
|
use support\Request;
|
||||||
use support\Response;
|
use support\Response;
|
||||||
use Tinywan\Jwt\JwtToken;
|
use Tinywan\Jwt\JwtToken;
|
||||||
@@ -54,6 +55,14 @@ class AuthTokenController extends BaseController
|
|||||||
return $this->fail('Signature verification failed', ReturnCode::FORBIDDEN);
|
return $this->fail('Signature verification failed', ReturnCode::FORBIDDEN);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$agent = SystemUser::where('agent_id', $agentId)->find();
|
||||||
|
if (!$agent || (int) ($agent->status ?? 0) !== 1) {
|
||||||
|
return $this->fail('Invalid agent_id', ReturnCode::FORBIDDEN);
|
||||||
|
}
|
||||||
|
if (empty($agent->dept_id) || (int) $agent->dept_id <= 0) {
|
||||||
|
return $this->fail('Agent channel is not configured', ReturnCode::FORBIDDEN);
|
||||||
|
}
|
||||||
|
|
||||||
$exp = (int) config('api.auth_token_exp', 86400);
|
$exp = (int) config('api.auth_token_exp', 86400);
|
||||||
$tokenResult = JwtToken::generateToken([
|
$tokenResult = JwtToken::generateToken([
|
||||||
'id' => 0,
|
'id' => 0,
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ use app\api\logic\UserLogic;
|
|||||||
use app\api\util\ReturnCode;
|
use app\api\util\ReturnCode;
|
||||||
use app\dice\model\game\DiceGame;
|
use app\dice\model\game\DiceGame;
|
||||||
use app\dice\model\player\DicePlayer;
|
use app\dice\model\player\DicePlayer;
|
||||||
use plugin\saiadmin\app\model\system\SystemUser;
|
|
||||||
use app\dice\model\play_record\DicePlayRecord;
|
use app\dice\model\play_record\DicePlayRecord;
|
||||||
use app\dice\model\player_wallet_record\DicePlayerWalletRecord;
|
use app\dice\model\player_wallet_record\DicePlayerWalletRecord;
|
||||||
use app\dice\model\player_ticket_record\DicePlayerTicketRecord;
|
use app\dice\model\player_ticket_record\DicePlayerTicketRecord;
|
||||||
@@ -60,7 +59,7 @@ class GameController extends BaseController
|
|||||||
public function getGameList(Request $request): Response
|
public function getGameList(Request $request): Response
|
||||||
{
|
{
|
||||||
$lang = $this->resolveLang($request->post('lang', 'zh'));
|
$lang = $this->resolveLang($request->post('lang', 'zh'));
|
||||||
$games = $this->buildPublicGameList($lang);
|
$games = $this->buildPublicGameList($lang, $this->agentDeptId($request));
|
||||||
return $this->success([
|
return $this->success([
|
||||||
'game_list' => $games,
|
'game_list' => $games,
|
||||||
]);
|
]);
|
||||||
@@ -73,7 +72,7 @@ class GameController extends BaseController
|
|||||||
public function getGameHall(Request $request): Response
|
public function getGameHall(Request $request): Response
|
||||||
{
|
{
|
||||||
$lang = $this->resolveLang($request->post('lang', 'zh'));
|
$lang = $this->resolveLang($request->post('lang', 'zh'));
|
||||||
$games = $this->buildPublicGameList($lang);
|
$games = $this->buildPublicGameList($lang, $this->agentDeptId($request));
|
||||||
$hallUrl = '';
|
$hallUrl = '';
|
||||||
if (!empty($games)) {
|
if (!empty($games)) {
|
||||||
$hallUrl = $games[0]['hall_url'] ?? '';
|
$hallUrl = $games[0]['hall_url'] ?? '';
|
||||||
@@ -93,36 +92,26 @@ class GameController extends BaseController
|
|||||||
public function getGameUrl(Request $request): Response
|
public function getGameUrl(Request $request): Response
|
||||||
{
|
{
|
||||||
$username = trim((string) ($request->post('username', '')));
|
$username = trim((string) ($request->post('username', '')));
|
||||||
$password = trim((string) ($request->post('password', '123456')));
|
|
||||||
$time = trim((string) ($request->post('time', '')));
|
$time = trim((string) ($request->post('time', '')));
|
||||||
|
|
||||||
if ($username === '') {
|
if ($username === '') {
|
||||||
return $this->fail('username is required', ReturnCode::PARAMS_ERROR);
|
return $this->fail('username is required', ReturnCode::PARAMS_ERROR);
|
||||||
}
|
}
|
||||||
if ($password === '') {
|
|
||||||
$password = '123456';
|
|
||||||
}
|
|
||||||
if ($time === '') {
|
if ($time === '') {
|
||||||
$time = (string) time();
|
$time = (string) time();
|
||||||
}
|
}
|
||||||
|
|
||||||
$adminId = null;
|
$deptId = $this->agentDeptId($request);
|
||||||
$adminIdsInTopDept = null;
|
$adminId = $this->agentAdminId($request);
|
||||||
$agentId = trim((string) ($request->agent_id ?? ''));
|
$adminIdsInTopDept = UserLogic::getAdminIdsByAgentIdTopDept(trim((string) ($request->agent_id ?? '')));
|
||||||
if ($agentId !== '') {
|
|
||||||
$systemUser = SystemUser::where('agent_id', $agentId)->find();
|
|
||||||
if ($systemUser) {
|
|
||||||
$adminId = (int) $systemUser->id;
|
|
||||||
$adminIdsInTopDept = UserLogic::getAdminIdsByAgentIdTopDept($agentId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$lang = trim((string) ($request->post('lang', 'zh')));
|
$lang = trim((string) ($request->post('lang', 'zh')));
|
||||||
$lang = in_array($lang, ['en', 'zh'], true) ? $lang : 'zh';
|
$lang = in_array($lang, ['en', 'zh'], true) ? $lang : 'zh';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$logic = new UserLogic();
|
$logic = new UserLogic();
|
||||||
$result = $logic->loginByUsername($username, $password, $lang, 0.0, $time, $adminId, $adminIdsInTopDept);
|
// 平台 v1 已通过 api-key + auth-token 双重校验,此处不再做 password 校验
|
||||||
|
$result = $logic->loginByUsername($username, '', $lang, 0.0, $time, $adminId, $adminIdsInTopDept, $deptId, true);
|
||||||
} catch (\plugin\saiadmin\exception\ApiException $e) {
|
} catch (\plugin\saiadmin\exception\ApiException $e) {
|
||||||
return $this->fail($e->getMessage(), ReturnCode::PARAMS_ERROR);
|
return $this->fail($e->getMessage(), ReturnCode::PARAMS_ERROR);
|
||||||
}
|
}
|
||||||
@@ -145,24 +134,36 @@ class GameController extends BaseController
|
|||||||
{
|
{
|
||||||
$usernameRaw = $request->input('username', '');
|
$usernameRaw = $request->input('username', '');
|
||||||
$username = is_string($usernameRaw) ? trim($usernameRaw) : '';
|
$username = is_string($usernameRaw) ? trim($usernameRaw) : '';
|
||||||
|
$deptId = $this->agentDeptId($request);
|
||||||
|
|
||||||
if ($username === '') {
|
if ($username === '') {
|
||||||
return $this->fail('username is required', ReturnCode::PARAMS_ERROR);
|
return $this->fail('username is required', ReturnCode::PARAMS_ERROR);
|
||||||
}
|
}
|
||||||
|
|
||||||
$cached = UserCache::getPlayerInfoSnapshotByUsername($username);
|
$cached = UserCache::getPlayerInfoSnapshotByUsername($this->scopedUsername($deptId, $username));
|
||||||
if ($cached !== null) {
|
if ($cached !== null) {
|
||||||
return $this->success($cached);
|
return $this->success($cached);
|
||||||
}
|
}
|
||||||
|
|
||||||
$player = DicePlayer::field(self::PLAYER_INFO_DB_FIELDS)->where('username', $username)->find();
|
try {
|
||||||
|
$logic = new UserLogic();
|
||||||
|
$player = $logic->findOrCreatePlayerByUsername(
|
||||||
|
$username,
|
||||||
|
$this->agentAdminId($request),
|
||||||
|
$deptId > 0 ? $deptId : null
|
||||||
|
);
|
||||||
|
} catch (\plugin\saiadmin\exception\ApiException $e) {
|
||||||
|
return $this->fail($e->getMessage(), ReturnCode::PARAMS_ERROR);
|
||||||
|
}
|
||||||
|
|
||||||
|
$player = DicePlayer::field(self::PLAYER_INFO_DB_FIELDS)->where('id', (int) $player->id)->find();
|
||||||
if (!$player) {
|
if (!$player) {
|
||||||
return $this->fail('User not found', ReturnCode::NOT_FOUND);
|
return $this->fail('User not found', ReturnCode::PARAMS_ERROR);
|
||||||
}
|
}
|
||||||
|
|
||||||
$hidden = ['password', 'lottery_config_id', 't1_weight', 't2_weight', 't3_weight', 't4_weight', 't5_weight', 'delete_time'];
|
$hidden = ['password', 'lottery_config_id', 't1_weight', 't2_weight', 't3_weight', 't4_weight', 't5_weight', 'delete_time'];
|
||||||
$info = $player->hidden($hidden)->toArray();
|
$info = $player->hidden($hidden)->toArray();
|
||||||
UserCache::setPlayerInfoSnapshotByUsername($username, $info);
|
UserCache::setPlayerInfoSnapshotByUsername($this->scopedUsername($deptId, $username), $info);
|
||||||
|
|
||||||
return $this->success($info);
|
return $this->success($info);
|
||||||
}
|
}
|
||||||
@@ -276,6 +277,7 @@ class GameController extends BaseController
|
|||||||
public function getPlayerGameRecord(Request $request): Response
|
public function getPlayerGameRecord(Request $request): Response
|
||||||
{
|
{
|
||||||
$username = trim((string) ($request->post('username', '')));
|
$username = trim((string) ($request->post('username', '')));
|
||||||
|
$deptId = $this->agentDeptId($request);
|
||||||
$startCreateTime = trim((string) ($request->post('start_create_time', '')));
|
$startCreateTime = trim((string) ($request->post('start_create_time', '')));
|
||||||
$endCreateTime = trim((string) ($request->post('end_create_time', '')));
|
$endCreateTime = trim((string) ($request->post('end_create_time', '')));
|
||||||
$window = $this->resolvePullRecordTimeWindow($startCreateTime, $endCreateTime);
|
$window = $this->resolvePullRecordTimeWindow($startCreateTime, $endCreateTime);
|
||||||
@@ -284,10 +286,10 @@ class GameController extends BaseController
|
|||||||
}
|
}
|
||||||
$limit = $this->resolvePullRecordLimit($request);
|
$limit = $this->resolvePullRecordLimit($request);
|
||||||
|
|
||||||
$query = DicePlayRecord::order('id', 'desc');
|
$query = DicePlayRecord::where('dept_id', $deptId)->order('id', 'desc');
|
||||||
|
|
||||||
if ($username !== '') {
|
if ($username !== '') {
|
||||||
$player = DicePlayer::where('username', $username)->find();
|
$player = $this->findPlayerByUsername($username, $deptId);
|
||||||
if (!$player) {
|
if (!$player) {
|
||||||
return $this->success([]);
|
return $this->success([]);
|
||||||
}
|
}
|
||||||
@@ -300,7 +302,7 @@ class GameController extends BaseController
|
|||||||
$list = $query->limit($limit)->select()->toArray();
|
$list = $query->limit($limit)->select()->toArray();
|
||||||
$playerIds = array_unique(array_column($list, 'player_id'));
|
$playerIds = array_unique(array_column($list, 'player_id'));
|
||||||
if (!empty($playerIds)) {
|
if (!empty($playerIds)) {
|
||||||
$players = DicePlayer::whereIn('id', $playerIds)->field('id,username,phone')->select()->toArray();
|
$players = DicePlayer::whereIn('id', $playerIds)->where('dept_id', $deptId)->field('id,username,phone')->select()->toArray();
|
||||||
$playerMap = [];
|
$playerMap = [];
|
||||||
foreach ($players as $p) {
|
foreach ($players as $p) {
|
||||||
$playerMap[(int) ($p['id'] ?? 0)] = $p;
|
$playerMap[(int) ($p['id'] ?? 0)] = $p;
|
||||||
@@ -321,6 +323,7 @@ class GameController extends BaseController
|
|||||||
public function getPlayerWalletRecord(Request $request): Response
|
public function getPlayerWalletRecord(Request $request): Response
|
||||||
{
|
{
|
||||||
$username = trim((string) ($request->post('username', '')));
|
$username = trim((string) ($request->post('username', '')));
|
||||||
|
$deptId = $this->agentDeptId($request);
|
||||||
$startCreateTime = trim((string) ($request->post('start_create_time', '')));
|
$startCreateTime = trim((string) ($request->post('start_create_time', '')));
|
||||||
$endCreateTime = trim((string) ($request->post('end_create_time', '')));
|
$endCreateTime = trim((string) ($request->post('end_create_time', '')));
|
||||||
$window = $this->resolvePullRecordTimeWindow($startCreateTime, $endCreateTime);
|
$window = $this->resolvePullRecordTimeWindow($startCreateTime, $endCreateTime);
|
||||||
@@ -329,10 +332,10 @@ class GameController extends BaseController
|
|||||||
}
|
}
|
||||||
$limit = $this->resolvePullRecordLimit($request);
|
$limit = $this->resolvePullRecordLimit($request);
|
||||||
|
|
||||||
$query = DicePlayerWalletRecord::order('id', 'desc');
|
$query = DicePlayerWalletRecord::where('dept_id', $deptId)->order('id', 'desc');
|
||||||
|
|
||||||
if ($username !== '') {
|
if ($username !== '') {
|
||||||
$player = DicePlayer::where('username', $username)->find();
|
$player = $this->findPlayerByUsername($username, $deptId);
|
||||||
if (!$player) {
|
if (!$player) {
|
||||||
return $this->success([]);
|
return $this->success([]);
|
||||||
}
|
}
|
||||||
@@ -357,6 +360,7 @@ class GameController extends BaseController
|
|||||||
public function getPlayerTicketRecord(Request $request): Response
|
public function getPlayerTicketRecord(Request $request): Response
|
||||||
{
|
{
|
||||||
$username = trim((string) ($request->post('username', '')));
|
$username = trim((string) ($request->post('username', '')));
|
||||||
|
$deptId = $this->agentDeptId($request);
|
||||||
$startCreateTime = trim((string) ($request->post('start_create_time', '')));
|
$startCreateTime = trim((string) ($request->post('start_create_time', '')));
|
||||||
$endCreateTime = trim((string) ($request->post('end_create_time', '')));
|
$endCreateTime = trim((string) ($request->post('end_create_time', '')));
|
||||||
$window = $this->resolvePullRecordTimeWindow($startCreateTime, $endCreateTime);
|
$window = $this->resolvePullRecordTimeWindow($startCreateTime, $endCreateTime);
|
||||||
@@ -365,10 +369,10 @@ class GameController extends BaseController
|
|||||||
}
|
}
|
||||||
$limit = $this->resolvePullRecordLimit($request);
|
$limit = $this->resolvePullRecordLimit($request);
|
||||||
|
|
||||||
$query = DicePlayerTicketRecord::order('id', 'desc');
|
$query = DicePlayerTicketRecord::where('dept_id', $deptId)->order('id', 'desc');
|
||||||
|
|
||||||
if ($username !== '') {
|
if ($username !== '') {
|
||||||
$player = DicePlayer::where('username', $username)->find();
|
$player = $this->findPlayerByUsername($username, $deptId);
|
||||||
if (!$player) {
|
if (!$player) {
|
||||||
return $this->success([]);
|
return $this->success([]);
|
||||||
}
|
}
|
||||||
@@ -394,6 +398,7 @@ class GameController extends BaseController
|
|||||||
public function setPlayerWallet(Request $request): Response
|
public function setPlayerWallet(Request $request): Response
|
||||||
{
|
{
|
||||||
$username = trim((string) ($request->post('username', '')));
|
$username = trim((string) ($request->post('username', '')));
|
||||||
|
$deptId = $this->agentDeptId($request);
|
||||||
$coin = $request->post('coin');
|
$coin = $request->post('coin');
|
||||||
|
|
||||||
if ($username === '') {
|
if ($username === '') {
|
||||||
@@ -408,9 +413,9 @@ class GameController extends BaseController
|
|||||||
return $this->fail('coin cannot be 0', ReturnCode::PARAMS_ERROR);
|
return $this->fail('coin cannot be 0', ReturnCode::PARAMS_ERROR);
|
||||||
}
|
}
|
||||||
|
|
||||||
$player = DicePlayer::where('username', $username)->find();
|
$player = $this->findPlayerByUsername($username, $deptId);
|
||||||
if (!$player) {
|
if (!$player) {
|
||||||
return $this->fail('User not found', ReturnCode::NOT_FOUND);
|
return $this->fail('User not found', ReturnCode::PARAMS_ERROR);
|
||||||
}
|
}
|
||||||
|
|
||||||
$walletBefore = (float) ($player->coin ?? 0);
|
$walletBefore = (float) ($player->coin ?? 0);
|
||||||
@@ -430,6 +435,7 @@ class GameController extends BaseController
|
|||||||
|
|
||||||
$adminId = ($player->admin_id ?? null) ? (int) $player->admin_id : null;
|
$adminId = ($player->admin_id ?? null) ? (int) $player->admin_id : null;
|
||||||
$record = DicePlayerWalletRecord::create([
|
$record = DicePlayerWalletRecord::create([
|
||||||
|
'dept_id' => $deptId,
|
||||||
'player_id' => (int) $player->id,
|
'player_id' => (int) $player->id,
|
||||||
'admin_id' => $adminId,
|
'admin_id' => $adminId,
|
||||||
'coin' => $coinVal,
|
'coin' => $coinVal,
|
||||||
@@ -452,6 +458,7 @@ class GameController extends BaseController
|
|||||||
UserCache::deleteUser($player->id);
|
UserCache::deleteUser($player->id);
|
||||||
if ($player->username !== '') {
|
if ($player->username !== '') {
|
||||||
UserCache::deletePlayerByUsername($player->username);
|
UserCache::deletePlayerByUsername($player->username);
|
||||||
|
UserCache::deletePlayerByUsername($this->scopedUsername($deptId, (string) $player->username));
|
||||||
}
|
}
|
||||||
|
|
||||||
$recordArr = $record->toArray();
|
$recordArr = $record->toArray();
|
||||||
@@ -471,13 +478,14 @@ class GameController extends BaseController
|
|||||||
return $langValue;
|
return $langValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function buildPublicGameList(string $lang): array
|
private function buildPublicGameList(string $lang, int $deptId): array
|
||||||
{
|
{
|
||||||
$rows = DiceGame::where('status', 1)
|
$rows = DiceGame::where('status', 1)
|
||||||
->orderBy('sort', 'asc')
|
->where('dept_id', $deptId)
|
||||||
->orderBy('id', 'asc')
|
->order('sort', 'asc')
|
||||||
->select(array_merge(self::GAME_PUBLIC_FIELDS, ['game_name', 'game_name_en']))
|
->order('id', 'asc')
|
||||||
->get()
|
->field(array_merge(self::GAME_PUBLIC_FIELDS, ['game_name', 'game_name_en']))
|
||||||
|
->select()
|
||||||
->toArray();
|
->toArray();
|
||||||
if (empty($rows)) {
|
if (empty($rows)) {
|
||||||
return [];
|
return [];
|
||||||
@@ -495,4 +503,26 @@ class GameController extends BaseController
|
|||||||
}
|
}
|
||||||
return $games;
|
return $games;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function agentDeptId(Request $request): int
|
||||||
|
{
|
||||||
|
return (int) ($request->agent_dept_id ?? 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function agentAdminId(Request $request): ?int
|
||||||
|
{
|
||||||
|
$adminId = (int) ($request->agent_admin_id ?? 0);
|
||||||
|
return $adminId > 0 ? $adminId : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function scopedUsername(int $deptId, string $username): string
|
||||||
|
{
|
||||||
|
return $deptId . ':' . $username;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function findPlayerByUsername(string $username, int $deptId): ?DicePlayer
|
||||||
|
{
|
||||||
|
$player = DicePlayer::where('username', $username)->where('dept_id', $deptId)->find();
|
||||||
|
return $player ?: null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,6 +48,9 @@ return [
|
|||||||
'没有原因' => 'Unknown reason',
|
'没有原因' => 'Unknown reason',
|
||||||
'缺少参数:agent_id、secret、time、signature 不能为空' => 'Missing parameters: agent_id, secret, time, signature are required',
|
'缺少参数:agent_id、secret、time、signature 不能为空' => 'Missing parameters: agent_id, secret, time, signature are required',
|
||||||
'服务端未配置 API_AUTH_TOKEN_SECRET' => 'API_AUTH_TOKEN_SECRET is not configured',
|
'服务端未配置 API_AUTH_TOKEN_SECRET' => 'API_AUTH_TOKEN_SECRET is not configured',
|
||||||
|
'服务端未配置 API_KEY' => 'API_KEY is not configured',
|
||||||
|
'请携带 api-key' => 'Please provide api-key',
|
||||||
|
'api-key 无效' => 'Invalid api-key',
|
||||||
'密钥错误' => 'Invalid secret',
|
'密钥错误' => 'Invalid secret',
|
||||||
'时间戳已过期或无效,请同步时间' => 'Timestamp expired or invalid, please sync time',
|
'时间戳已过期或无效,请同步时间' => 'Timestamp expired or invalid, please sync time',
|
||||||
'签名验证失败' => 'Signature verification failed',
|
'签名验证失败' => 'Signature verification failed',
|
||||||
|
|||||||
@@ -70,10 +70,14 @@ class GameLogic
|
|||||||
UserCache::setUser($playerId, $updatedUserArr);
|
UserCache::setUser($playerId, $updatedUserArr);
|
||||||
|
|
||||||
$adminId = ($player->admin_id ?? null) ? (int) $player->admin_id : null;
|
$adminId = ($player->admin_id ?? null) ? (int) $player->admin_id : null;
|
||||||
|
$playerDeptId = isset($player->dept_id) && $player->dept_id !== null && $player->dept_id !== ''
|
||||||
|
? (int) $player->dept_id
|
||||||
|
: null;
|
||||||
try {
|
try {
|
||||||
Db::transaction(function () use (
|
Db::transaction(function () use (
|
||||||
$player,
|
$player,
|
||||||
$playerId,
|
$playerId,
|
||||||
|
$playerDeptId,
|
||||||
$adminId,
|
$adminId,
|
||||||
$cost,
|
$cost,
|
||||||
$coinBefore,
|
$coinBefore,
|
||||||
@@ -93,6 +97,7 @@ class GameLogic
|
|||||||
|
|
||||||
DicePlayerWalletRecord::create([
|
DicePlayerWalletRecord::create([
|
||||||
'player_id' => $playerId,
|
'player_id' => $playerId,
|
||||||
|
'dept_id' => $playerDeptId,
|
||||||
'admin_id' => $adminId,
|
'admin_id' => $adminId,
|
||||||
'coin' => round(-$cost, 2),
|
'coin' => round(-$cost, 2),
|
||||||
'type' => self::WALLET_TYPE_BUY_DRAW,
|
'type' => self::WALLET_TYPE_BUY_DRAW,
|
||||||
@@ -106,6 +111,7 @@ class GameLogic
|
|||||||
|
|
||||||
DicePlayerTicketRecord::create([
|
DicePlayerTicketRecord::create([
|
||||||
'player_id' => $playerId,
|
'player_id' => $playerId,
|
||||||
|
'dept_id' => $playerDeptId,
|
||||||
'admin_id' => $adminId,
|
'admin_id' => $adminId,
|
||||||
'use_coins' => round($cost, 2),
|
'use_coins' => round($cost, 2),
|
||||||
'ante' => 1,
|
'ante' => 1,
|
||||||
|
|||||||
@@ -50,6 +50,8 @@ class PlayStartLogic
|
|||||||
private const SUPER_WIN_GRID_NUMBERS = [5, 10, 15, 20, 25, 30];
|
private const SUPER_WIN_GRID_NUMBERS = [5, 10, 15, 20, 25, 30];
|
||||||
/** 5 和 30 抽到即豹子,不参与 BIGWIN 权重判定;10/15/20/25 按 BIGWIN weight 判定是否豹子 */
|
/** 5 和 30 抽到即豹子,不参与 BIGWIN 权重判定;10/15/20/25 按 BIGWIN weight 判定是否豹子 */
|
||||||
private const SUPER_WIN_ALWAYS_GRID_NUMBERS = [5, 30];
|
private const SUPER_WIN_ALWAYS_GRID_NUMBERS = [5, 30];
|
||||||
|
/** T4 惩罚格余额不足时写入游玩记录的备注 */
|
||||||
|
private const REMARK_T4_INSUFFICIENT_BALANCE = '惩罚格奖励:玩家余额不足,已扣尽钱包剩余余额';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 执行一局游戏
|
* 执行一局游戏
|
||||||
@@ -66,6 +68,9 @@ class PlayStartLogic
|
|||||||
}
|
}
|
||||||
|
|
||||||
$configDeptId = AdminScopeHelper::resolvePlayerConfigDeptId($player);
|
$configDeptId = AdminScopeHelper::resolvePlayerConfigDeptId($player);
|
||||||
|
$playerDeptId = isset($player->dept_id) && $player->dept_id !== null && $player->dept_id !== ''
|
||||||
|
? (int) $player->dept_id
|
||||||
|
: null;
|
||||||
|
|
||||||
$coin = (float) $player->coin;
|
$coin = (float) $player->coin;
|
||||||
if ($ante <= 0) {
|
if ($ante <= 0) {
|
||||||
@@ -113,7 +118,8 @@ class PlayStartLogic
|
|||||||
}
|
}
|
||||||
|
|
||||||
$configType0 = DiceLotteryPoolConfig::where('name', 'default')->where('dept_id', $configDeptId)->find();
|
$configType0 = DiceLotteryPoolConfig::where('name', 'default')->where('dept_id', $configDeptId)->find();
|
||||||
$configType1 = DiceLotteryPoolConfig::where('name', 'killScore')->where('dept_id', $configDeptId)->find();
|
$configKill = DiceLotteryPoolConfig::where('name', 'killScore')->where('dept_id', $configDeptId)->find();
|
||||||
|
$configFree = DiceLotteryPoolConfig::where('name', 'free')->where('dept_id', $configDeptId)->find();
|
||||||
if (!$configType0) {
|
if (!$configType0) {
|
||||||
throw new ApiException('Lottery pool config not found (name=default required)');
|
throw new ApiException('Lottery pool config not found (name=default required)');
|
||||||
}
|
}
|
||||||
@@ -121,41 +127,40 @@ class PlayStartLogic
|
|||||||
// 付费抽奖:开始前扣除费用 ante * UNIT_COST
|
// 付费抽奖:开始前扣除费用 ante * UNIT_COST
|
||||||
$paidAmount = $ticketType === self::LOTTERY_TYPE_PAID ? round($ante * self::UNIT_COST, 2) : 0.0;
|
$paidAmount = $ticketType === self::LOTTERY_TYPE_PAID ? round($ante * self::UNIT_COST, 2) : 0.0;
|
||||||
|
|
||||||
// 游玩前余额校验(按 T4 惩罚最大值兜底):
|
// 付费抽奖:余额不足单局费用(ante * UNIT_COST)时不允许开始;惩罚格不足部分在局内扣尽剩余余额
|
||||||
// 门槛 = paidAmount(压注*1) + abs(T4最小real_ev)*ante
|
if ($ticketType === self::LOTTERY_TYPE_PAID && $coin < $paidAmount) {
|
||||||
$t4List = DiceRewardConfig::getCachedByTier('T4', $configDeptId);
|
|
||||||
$t4MinRealEv = null;
|
|
||||||
foreach ($t4List as $row) {
|
|
||||||
$ev = $row['real_ev'] ?? null;
|
|
||||||
if ($ev === null || $ev === '') {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
$evFloat = filter_var($ev, FILTER_VALIDATE_FLOAT);
|
|
||||||
if ($evFloat === false) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if ($t4MinRealEv === null || $evFloat < $t4MinRealEv) {
|
|
||||||
$t4MinRealEv = $evFloat;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
$t4PenaltyAbs = $t4MinRealEv === null ? 0.0 : abs($t4MinRealEv) * $ante;
|
|
||||||
$needMinBalance = round($paidAmount + $t4PenaltyAbs, 2);
|
|
||||||
if ($coin < $needMinBalance) {
|
|
||||||
throw new ApiException('余额不足');
|
throw new ApiException('余额不足');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 彩金池累计盈利:用于判断是否触发杀分(不再依赖单个玩家累计盈利)
|
// 彩金池累计盈利:用于判断是否触发杀分(不再依赖单个玩家累计盈利)
|
||||||
// 该值来自 dice_lottery_pool_config.profit_amount
|
// 该值来自 dice_lottery_pool_config.profit_amount(default 奖池)
|
||||||
$poolProfitTotal = $configType0->profit_amount ?? 0;
|
$poolProfitTotal = $configType0->profit_amount ?? 0;
|
||||||
$safetyLine = (int) ($configType0->safety_line ?? 0);
|
$safetyLine = (int) ($configType0->safety_line ?? 0);
|
||||||
$killEnabled = ((int) ($configType0->kill_enabled ?? 1)) === 1;
|
$killEnabled = ((int) ($configType0->kill_enabled ?? 1)) === 1;
|
||||||
// 盈利>=安全线且开启杀分:付费/免费都用 killScore;盈利<安全线:付费用玩家权重,免费用 killScore(无则用 default)
|
|
||||||
// 记录 lottery_config_id:用池权重时记对应池,付费用玩家权重时记 default
|
$usePaidKill = $ticketType === self::LOTTERY_TYPE_PAID
|
||||||
$usePoolWeights = ($ticketType === self::LOTTERY_TYPE_PAID && $killEnabled && $poolProfitTotal >= $safetyLine && $configType1 !== null)
|
&& $killEnabled
|
||||||
|| ($ticketType === self::LOTTERY_TYPE_FREE);
|
&& $poolProfitTotal >= $safetyLine
|
||||||
$config = $usePoolWeights
|
&& $configKill !== null;
|
||||||
? (($ticketType === self::LOTTERY_TYPE_FREE && $configType1 === null) ? $configType0 : $configType1)
|
|
||||||
: $configType0;
|
$playerLinkedPool = $this->resolvePlayerLinkedPoolConfig($player, $configDeptId);
|
||||||
|
|
||||||
|
if ($ticketType === self::LOTTERY_TYPE_FREE) {
|
||||||
|
// 免费抽奖券:使用本渠道 name=free 奖池档位权重;无 free 时回退 default
|
||||||
|
$config = $configFree ?? $configType0;
|
||||||
|
$usePoolWeights = true;
|
||||||
|
} elseif ($usePaidKill) {
|
||||||
|
$config = $configKill;
|
||||||
|
$usePoolWeights = true;
|
||||||
|
} else {
|
||||||
|
// 付费未触发杀分:关联 playerDefault 时实时读该池权重;否则按玩家行内 T*_weight
|
||||||
|
$config = $configType0;
|
||||||
|
$usePoolWeights = false;
|
||||||
|
if ($playerLinkedPool !== null && $playerLinkedPool->isPlayerDefaultTemplate()) {
|
||||||
|
$usePoolWeights = true;
|
||||||
|
$config = $playerLinkedPool;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 按档位 T1-T5 抽取后,从 DiceReward 表按当前方向取该档位数据,再按 weight 抽取一条得到 grid_number
|
// 按档位 T1-T5 抽取后,从 DiceReward 表按当前方向取该档位数据,再按 weight 抽取一条得到 grid_number
|
||||||
$rewardInstance = DiceReward::getCachedInstance($configDeptId);
|
$rewardInstance = DiceReward::getCachedInstance($configDeptId);
|
||||||
@@ -200,9 +205,9 @@ class PlayStartLogic
|
|||||||
$rollNumber = (int) ($chosen['grid_number'] ?? 0);
|
$rollNumber = (int) ($chosen['grid_number'] ?? 0);
|
||||||
$realEv = (float) ($chosen['real_ev'] ?? 0);
|
$realEv = (float) ($chosen['real_ev'] ?? 0);
|
||||||
// T5/再来一次:以奖励行 tier 为准,并以摇奖档位 $tier 兜底(与 reward_tier 展示一致,避免 dice_reward 行缺 tier 时不发券)
|
// T5/再来一次:以奖励行 tier 为准,并以摇奖档位 $tier 兜底(与 reward_tier 展示一致,避免 dice_reward 行缺 tier 时不发券)
|
||||||
$isTierT5 = (string) ($chosen['tier'] ?? '') === 'T5';
|
$isTierPlayAgain = (string) ($chosen['tier'] ?? '') === 'T5';
|
||||||
if ($isTierT5 === false && (string) ($tier ?? '') === 'T5') {
|
if ($isTierPlayAgain === false && (string) ($tier ?? '') === 'T5') {
|
||||||
$isTierT5 = true;
|
$isTierPlayAgain = true;
|
||||||
}
|
}
|
||||||
// 摇色子中奖:按 dice_reward_config.real_ev 直接结算(已乘 ante)
|
// 摇色子中奖:按 dice_reward_config.real_ev 直接结算(已乘 ante)
|
||||||
$rewardWinCoin = round($realEv * $ante, 2);
|
$rewardWinCoin = round($realEv * $ante, 2);
|
||||||
@@ -243,7 +248,7 @@ class PlayStartLogic
|
|||||||
// 中 BIGWIN 豹子:不走原奖励流程,不记录原奖励,不触发 T5 再来一次,仅发放豹子奖金
|
// 中 BIGWIN 豹子:不走原奖励流程,不记录原奖励,不触发 T5 再来一次,仅发放豹子奖金
|
||||||
$rewardWinCoin = 0.0;
|
$rewardWinCoin = 0.0;
|
||||||
$realEv = 0.0;
|
$realEv = 0.0;
|
||||||
$isTierT5 = false;
|
$isTierPlayAgain = false;
|
||||||
} else {
|
} else {
|
||||||
$rollArray = $this->generateNonSuperWinRollArrayWithSum($rollNumber);
|
$rollArray = $this->generateNonSuperWinRollArrayWithSum($rollNumber);
|
||||||
}
|
}
|
||||||
@@ -262,7 +267,11 @@ class PlayStartLogic
|
|||||||
$winCoin = round($superWinCoin + $rewardWinCoin, 2); // 赢取平台币 = 中大奖 + 摇色子中奖(豹子时 rewardWinCoin 已为 0)
|
$winCoin = round($superWinCoin + $rewardWinCoin, 2); // 赢取平台币 = 中大奖 + 摇色子中奖(豹子时 rewardWinCoin 已为 0)
|
||||||
|
|
||||||
$record = null;
|
$record = null;
|
||||||
|
$settledWinCoin = $winCoin;
|
||||||
$configId = (int) $config->id;
|
$configId = (int) $config->id;
|
||||||
|
if ($ticketType === self::LOTTERY_TYPE_PAID && !$usePaidKill && $playerLinkedPool !== null) {
|
||||||
|
$configId = (int) $playerLinkedPool->id;
|
||||||
|
}
|
||||||
$type0ConfigId = (int) $configType0->id;
|
$type0ConfigId = (int) $configType0->id;
|
||||||
$rewardId = ($isWin === 1 && $superWinCoin > 0) ? 0 : $targetIndex; // 中豹子不记录原奖励配置 id
|
$rewardId = ($isWin === 1 && $superWinCoin > 0) ? 0 : $targetIndex; // 中豹子不记录原奖励配置 id
|
||||||
$configName = (string) ($config->name ?? '');
|
$configName = (string) ($config->name ?? '');
|
||||||
@@ -270,6 +279,7 @@ class PlayStartLogic
|
|||||||
try {
|
try {
|
||||||
Db::transaction(function () use (
|
Db::transaction(function () use (
|
||||||
$playerId,
|
$playerId,
|
||||||
|
$playerDeptId,
|
||||||
$adminId,
|
$adminId,
|
||||||
$configId,
|
$configId,
|
||||||
$type0ConfigId,
|
$type0ConfigId,
|
||||||
@@ -287,30 +297,12 @@ class PlayStartLogic
|
|||||||
$startIndex,
|
$startIndex,
|
||||||
$targetIndex,
|
$targetIndex,
|
||||||
$rollArray,
|
$rollArray,
|
||||||
$isTierT5,
|
$isTierPlayAgain,
|
||||||
$tier,
|
$tier,
|
||||||
&$record
|
&$record,
|
||||||
|
&$settledWinCoin
|
||||||
) {
|
) {
|
||||||
$rewardTier = ($isWin === 1 && $superWinCoin > 0) ? 'BIGWIN' : (string) ($tier ?? '');
|
$rewardTier = ($isWin === 1 && $superWinCoin > 0) ? 'BIGWIN' : (string) ($tier ?? '');
|
||||||
$record = DicePlayRecord::create([
|
|
||||||
'player_id' => $playerId,
|
|
||||||
'admin_id' => $adminId,
|
|
||||||
'lottery_config_id' => $configId,
|
|
||||||
'lottery_type' => $ticketType,
|
|
||||||
'ante' => $ante,
|
|
||||||
'paid_amount' => $paidAmount,
|
|
||||||
'is_win' => $isWin,
|
|
||||||
'win_coin' => $winCoin,
|
|
||||||
'super_win_coin' => $superWinCoin,
|
|
||||||
'reward_win_coin' => $rewardWinCoin,
|
|
||||||
'direction' => $direction,
|
|
||||||
'reward_tier' => $rewardTier,
|
|
||||||
'start_index' => $startIndex,
|
|
||||||
'target_index' => $targetIndex,
|
|
||||||
'roll_array' => is_array($rollArray) ? json_encode($rollArray) : $rollArray,
|
|
||||||
'roll_number' => is_array($rollArray) ? array_sum($rollArray) : 0,
|
|
||||||
'status' => self::RECORD_STATUS_SUCCESS,
|
|
||||||
]);
|
|
||||||
|
|
||||||
$p = DicePlayer::find($playerId);
|
$p = DicePlayer::find($playerId);
|
||||||
if (!$p) {
|
if (!$p) {
|
||||||
@@ -319,10 +311,41 @@ class PlayStartLogic
|
|||||||
$coinBefore = (float) $p->coin;
|
$coinBefore = (float) $p->coin;
|
||||||
// 开始前先扣付费金额,再加中奖金额(免费抽奖 paid_amount=0)
|
// 开始前先扣付费金额,再加中奖金额(免费抽奖 paid_amount=0)
|
||||||
$coinAfter = round($coinBefore - $paidAmount + $winCoin, 2);
|
$coinAfter = round($coinBefore - $paidAmount + $winCoin, 2);
|
||||||
// T4 惩罚兜底:扣完购券费用后若余额不足以承受本次惩罚(导致为负),统一按“余额不足”提示
|
$recordWinCoin = $winCoin;
|
||||||
|
$recordRewardWinCoin = $rewardWinCoin;
|
||||||
|
$playRecordRemark = null;
|
||||||
|
// T4 惩罚:扣完购券费用后若不足以支付全额惩罚,则扣尽钱包剩余余额并记录备注
|
||||||
if ($rewardTier === 'T4' && $coinAfter < 0) {
|
if ($rewardTier === 'T4' && $coinAfter < 0) {
|
||||||
throw new ApiException('余额不足');
|
$walletRemain = round($coinBefore - $paidAmount, 2);
|
||||||
|
$coinAfter = 0.0;
|
||||||
|
$recordWinCoin = round(-$walletRemain, 2);
|
||||||
|
$recordRewardWinCoin = $recordWinCoin;
|
||||||
|
$playRecordRemark = self::REMARK_T4_INSUFFICIENT_BALANCE;
|
||||||
}
|
}
|
||||||
|
$settledWinCoin = $recordWinCoin;
|
||||||
|
|
||||||
|
$record = DicePlayRecord::create([
|
||||||
|
'player_id' => $playerId,
|
||||||
|
'dept_id' => $playerDeptId,
|
||||||
|
'admin_id' => $adminId,
|
||||||
|
'lottery_config_id' => $configId,
|
||||||
|
'lottery_type' => $ticketType,
|
||||||
|
'ante' => $ante,
|
||||||
|
'paid_amount' => $paidAmount,
|
||||||
|
'is_win' => $isWin,
|
||||||
|
'win_coin' => $recordWinCoin,
|
||||||
|
'super_win_coin' => $superWinCoin,
|
||||||
|
'reward_win_coin' => $recordRewardWinCoin,
|
||||||
|
'direction' => $direction,
|
||||||
|
'reward_tier' => $rewardTier,
|
||||||
|
'start_index' => $startIndex,
|
||||||
|
'target_index' => $targetIndex,
|
||||||
|
'roll_array' => is_array($rollArray) ? json_encode($rollArray) : $rollArray,
|
||||||
|
'roll_number' => is_array($rollArray) ? array_sum($rollArray) : 0,
|
||||||
|
'status' => self::RECORD_STATUS_SUCCESS,
|
||||||
|
'remark' => $playRecordRemark,
|
||||||
|
]);
|
||||||
|
|
||||||
$p->coin = $coinAfter;
|
$p->coin = $coinAfter;
|
||||||
// 免费抽奖消耗:优先消耗 free_ticket.count,耗尽则清空 free_ticket;否则兼容旧 free_ticket_count
|
// 免费抽奖消耗:优先消耗 free_ticket.count,耗尽则清空 free_ticket;否则兼容旧 free_ticket_count
|
||||||
if ($ticketType === self::LOTTERY_TYPE_FREE) {
|
if ($ticketType === self::LOTTERY_TYPE_FREE) {
|
||||||
@@ -357,6 +380,7 @@ class PlayStartLogic
|
|||||||
$freeCnt = $isPaidPlay ? 0 : 1;
|
$freeCnt = $isPaidPlay ? 0 : 1;
|
||||||
DicePlayerTicketRecord::create([
|
DicePlayerTicketRecord::create([
|
||||||
'player_id' => $playerId,
|
'player_id' => $playerId,
|
||||||
|
'dept_id' => $playerDeptId,
|
||||||
'admin_id' => $adminId,
|
'admin_id' => $adminId,
|
||||||
'use_coins' => $paidAmount,
|
'use_coins' => $paidAmount,
|
||||||
'ante' => $ante,
|
'ante' => $ante,
|
||||||
@@ -369,7 +393,7 @@ class PlayStartLogic
|
|||||||
// 若本局中奖档位为 T5,则额外赠送 1 次免费抽奖次数:
|
// 若本局中奖档位为 T5,则额外赠送 1 次免费抽奖次数:
|
||||||
// - 新结构:写入 free_ticket(ante=本局注数,count+1)
|
// - 新结构:写入 free_ticket(ante=本局注数,count+1)
|
||||||
// - 兼容旧结构:free_ticket_count +1
|
// - 兼容旧结构:free_ticket_count +1
|
||||||
if ($isTierT5) {
|
if ($isTierPlayAgain) {
|
||||||
$ft = $p->free_ticket ?? null;
|
$ft = $p->free_ticket ?? null;
|
||||||
$ftAnte = null;
|
$ftAnte = null;
|
||||||
$ftCount = 0;
|
$ftCount = 0;
|
||||||
@@ -395,6 +419,7 @@ class PlayStartLogic
|
|||||||
|
|
||||||
DicePlayerTicketRecord::create([
|
DicePlayerTicketRecord::create([
|
||||||
'player_id' => $playerId,
|
'player_id' => $playerId,
|
||||||
|
'dept_id' => $playerDeptId,
|
||||||
'admin_id' => $adminId,
|
'admin_id' => $adminId,
|
||||||
'ante' => $ante,
|
'ante' => $ante,
|
||||||
'free_ticket_count' => 1,
|
'free_ticket_count' => 1,
|
||||||
@@ -422,7 +447,7 @@ class PlayStartLogic
|
|||||||
// 彩金池累计盈利累加在 name=default 彩金池上:
|
// 彩金池累计盈利累加在 name=default 彩金池上:
|
||||||
// 付费:每局按「本局赢取平台币 win_coin - 抽奖费用 paid_amount(ante*UNIT_COST)」
|
// 付费:每局按「本局赢取平台币 win_coin - 抽奖费用 paid_amount(ante*UNIT_COST)」
|
||||||
// 免费券:paid_amount=0,只计入 win_coin
|
// 免费券:paid_amount=0,只计入 win_coin
|
||||||
$perPlayProfit = ($ticketType === self::LOTTERY_TYPE_PAID) ? ($winCoin - $paidAmount) : $winCoin;
|
$perPlayProfit = ($ticketType === self::LOTTERY_TYPE_PAID) ? ($recordWinCoin - $paidAmount) : $recordWinCoin;
|
||||||
$addProfit = round($perPlayProfit, 2);
|
$addProfit = round($perPlayProfit, 2);
|
||||||
try {
|
try {
|
||||||
DiceLotteryPoolConfig::where('id', $type0ConfigId)->update([
|
DiceLotteryPoolConfig::where('id', $type0ConfigId)->update([
|
||||||
@@ -441,6 +466,7 @@ class PlayStartLogic
|
|||||||
$walletAfterBuy = round($coinBefore - $paidAmount, 2);
|
$walletAfterBuy = round($coinBefore - $paidAmount, 2);
|
||||||
DicePlayerWalletRecord::create([
|
DicePlayerWalletRecord::create([
|
||||||
'player_id' => $playerId,
|
'player_id' => $playerId,
|
||||||
|
'dept_id' => $playerDeptId,
|
||||||
'admin_id' => $adminId,
|
'admin_id' => $adminId,
|
||||||
'coin' => round(-$paidAmount, 2),
|
'coin' => round(-$paidAmount, 2),
|
||||||
'type' => self::WALLET_TYPE_BUY_DRAW,
|
'type' => self::WALLET_TYPE_BUY_DRAW,
|
||||||
@@ -451,11 +477,15 @@ class PlayStartLogic
|
|||||||
}
|
}
|
||||||
|
|
||||||
$walletBeforeDraw = $coinBefore - $paidAmount;
|
$walletBeforeDraw = $coinBefore - $paidAmount;
|
||||||
$drawRemark = ($winCoin >= 0 ? '抽奖中奖' : '抽奖惩罚') . '|play_record_id=' . $record->id;
|
$drawRemark = ($recordWinCoin >= 0 ? '抽奖中奖' : '抽奖惩罚') . '|play_record_id=' . $record->id;
|
||||||
|
if ($playRecordRemark !== null) {
|
||||||
|
$drawRemark .= '|' . $playRecordRemark;
|
||||||
|
}
|
||||||
DicePlayerWalletRecord::create([
|
DicePlayerWalletRecord::create([
|
||||||
'player_id' => $playerId,
|
'player_id' => $playerId,
|
||||||
|
'dept_id' => $playerDeptId,
|
||||||
'admin_id' => $adminId,
|
'admin_id' => $adminId,
|
||||||
'coin' => $winCoin,
|
'coin' => $recordWinCoin,
|
||||||
'type' => self::WALLET_TYPE_DRAW,
|
'type' => self::WALLET_TYPE_DRAW,
|
||||||
'wallet_before' => round($walletBeforeDraw, 2),
|
'wallet_before' => round($walletBeforeDraw, 2),
|
||||||
'wallet_after' => $coinAfter,
|
'wallet_after' => $coinAfter,
|
||||||
@@ -467,6 +497,7 @@ class PlayStartLogic
|
|||||||
try {
|
try {
|
||||||
$record = DicePlayRecord::create([
|
$record = DicePlayRecord::create([
|
||||||
'player_id' => $playerId,
|
'player_id' => $playerId,
|
||||||
|
'dept_id' => $playerDeptId,
|
||||||
'admin_id' => $adminId ?? null,
|
'admin_id' => $adminId ?? null,
|
||||||
'lottery_config_id' => $configId ?? 0,
|
'lottery_config_id' => $configId ?? 0,
|
||||||
'lottery_type' => $ticketType,
|
'lottery_type' => $ticketType,
|
||||||
@@ -644,6 +675,7 @@ class PlayStartLogic
|
|||||||
*/
|
*/
|
||||||
public function simulateOnePlay($config, int $direction, int $lotteryType = 0, int $ante = 1, ?array $customTierWeights = null, ?int $configDeptId = null): array
|
public function simulateOnePlay($config, int $direction, int $lotteryType = 0, int $ante = 1, ?array $customTierWeights = null, ?int $configDeptId = null): array
|
||||||
{
|
{
|
||||||
|
$useKillMode = false;
|
||||||
$rewardInstance = DiceReward::getCachedInstance($configDeptId);
|
$rewardInstance = DiceReward::getCachedInstance($configDeptId);
|
||||||
$byTierDirection = $rewardInstance['by_tier_direction'] ?? [];
|
$byTierDirection = $rewardInstance['by_tier_direction'] ?? [];
|
||||||
$maxTierRetry = 10;
|
$maxTierRetry = 10;
|
||||||
@@ -766,4 +798,24 @@ class PlayStartLogic
|
|||||||
'grants_free_ticket' => $grantsFreeTicket,
|
'grants_free_ticket' => $grantsFreeTicket,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 玩家已选彩金池配置(用于 playerDefault 运行时权重)
|
||||||
|
*/
|
||||||
|
private function resolvePlayerLinkedPoolConfig(DicePlayer $player, int $configDeptId): ?DiceLotteryPoolConfig
|
||||||
|
{
|
||||||
|
$linkedId = (int) ($player->lottery_config_id ?? 0);
|
||||||
|
if ($linkedId <= 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
$cfg = DiceLotteryPoolConfig::find($linkedId);
|
||||||
|
if (!$cfg) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
$poolDeptId = AdminScopeHelper::normalizeRecordDeptId($cfg->dept_id ?? null);
|
||||||
|
if ($poolDeptId !== AdminScopeHelper::normalizeRecordDeptId($configDeptId)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return $cfg;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -69,14 +69,12 @@ class UserLogic
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 登录(JSON:username, password, lang, coin, time)
|
* 按用户名查找玩家;不存在则创建并绑定渠道/管理员(供 getPlayerInfo 等接口)
|
||||||
* 存在则校验密码并更新 coin(累加);不存在则创建用户并写入 coin。
|
|
||||||
* 将会话写入 Redis,返回 token 与前端连接地址。
|
|
||||||
*
|
*
|
||||||
* @param int|null $adminId 创建新用户时关联的后台管理员ID(sa_system_user.id),可选
|
* @param int|null $adminId 关联后台管理员 ID(sa_system_user.id)
|
||||||
* @param int[]|null $adminIdsInTopDept 当前管理员顶级部门下的所有管理员ID,用于按部门范围查找玩家;为空时退化为仅按 username 查找
|
* @param int|null $deptId 所属渠道 ID
|
||||||
*/
|
*/
|
||||||
public function loginByUsername(string $username, string $password, string $lang, float $coin, string $time, ?int $adminId = null, ?array $adminIdsInTopDept = null): array
|
public function findOrCreatePlayerByUsername(string $username, ?int $adminId = null, ?int $deptId = null): DicePlayer
|
||||||
{
|
{
|
||||||
$username = trim($username);
|
$username = trim($username);
|
||||||
if ($username === '') {
|
if ($username === '') {
|
||||||
@@ -84,6 +82,59 @@ class UserLogic
|
|||||||
}
|
}
|
||||||
|
|
||||||
$query = DicePlayer::where('username', $username);
|
$query = DicePlayer::where('username', $username);
|
||||||
|
if ($deptId !== null && $deptId > 0) {
|
||||||
|
$query->where('dept_id', $deptId);
|
||||||
|
}
|
||||||
|
$player = $query->find();
|
||||||
|
if ($player) {
|
||||||
|
if ((int) ($player->status ?? 1) === 0) {
|
||||||
|
throw new ApiException('Account is disabled');
|
||||||
|
}
|
||||||
|
return $player;
|
||||||
|
}
|
||||||
|
|
||||||
|
$player = new DicePlayer();
|
||||||
|
$player->username = $username;
|
||||||
|
$player->phone = $username;
|
||||||
|
$player->password = $this->hashPassword('123456');
|
||||||
|
$player->status = self::STATUS_NORMAL;
|
||||||
|
$player->coin = 0;
|
||||||
|
if ($deptId !== null && $deptId > 0) {
|
||||||
|
$player->dept_id = $deptId;
|
||||||
|
}
|
||||||
|
if ($adminId !== null && $adminId > 0) {
|
||||||
|
$player->admin_id = $adminId;
|
||||||
|
if ($deptId === null || $deptId <= 0) {
|
||||||
|
$adminUser = SystemUser::find($adminId);
|
||||||
|
if ($adminUser && !empty($adminUser->dept_id)) {
|
||||||
|
$player->dept_id = $adminUser->dept_id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$player->save();
|
||||||
|
|
||||||
|
return $player;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 登录(JSON:username, password, lang, coin, time)
|
||||||
|
* 存在则校验密码并更新 coin(累加);不存在则创建用户并写入 coin。
|
||||||
|
* 将会话写入 Redis,返回 token 与前端连接地址。
|
||||||
|
*
|
||||||
|
* @param int|null $adminId 创建新用户时关联的后台管理员ID(sa_system_user.id),可选
|
||||||
|
* @param int[]|null $adminIdsInTopDept 当前管理员顶级部门下的所有管理员ID,用于按部门范围查找玩家;为空时退化为仅按 username 查找
|
||||||
|
*/
|
||||||
|
public function loginByUsername(string $username, string $password, string $lang, float $coin, string $time, ?int $adminId = null, ?array $adminIdsInTopDept = null, ?int $deptId = null, bool $skipPasswordValidation = false): array
|
||||||
|
{
|
||||||
|
$username = trim($username);
|
||||||
|
if ($username === '') {
|
||||||
|
throw new ApiException('username is required');
|
||||||
|
}
|
||||||
|
|
||||||
|
$query = DicePlayer::where('username', $username);
|
||||||
|
if ($deptId !== null && $deptId > 0) {
|
||||||
|
$query->where('dept_id', $deptId);
|
||||||
|
}
|
||||||
if ($adminIdsInTopDept !== null && !empty($adminIdsInTopDept)) {
|
if ($adminIdsInTopDept !== null && !empty($adminIdsInTopDept)) {
|
||||||
$query->whereIn('admin_id', $adminIdsInTopDept);
|
$query->whereIn('admin_id', $adminIdsInTopDept);
|
||||||
}
|
}
|
||||||
@@ -92,10 +143,12 @@ class UserLogic
|
|||||||
if ((int) ($player->status ?? 1) === 0) {
|
if ((int) ($player->status ?? 1) === 0) {
|
||||||
throw new ApiException('Account is disabled and cannot log in');
|
throw new ApiException('Account is disabled and cannot log in');
|
||||||
}
|
}
|
||||||
|
if (!$skipPasswordValidation) {
|
||||||
$hashed = $this->hashPassword($password);
|
$hashed = $this->hashPassword($password);
|
||||||
if ($player->password !== $hashed) {
|
if ($player->password !== $hashed) {
|
||||||
throw new ApiException('Wrong password');
|
throw new ApiException('Wrong password');
|
||||||
}
|
}
|
||||||
|
}
|
||||||
$currentCoin = (float) $player->coin;
|
$currentCoin = (float) $player->coin;
|
||||||
$player->coin = $currentCoin + $coin;
|
$player->coin = $currentCoin + $coin;
|
||||||
$player->save();
|
$player->save();
|
||||||
@@ -106,10 +159,13 @@ class UserLogic
|
|||||||
$player->password = $this->hashPassword($password);
|
$player->password = $this->hashPassword($password);
|
||||||
$player->status = self::STATUS_NORMAL;
|
$player->status = self::STATUS_NORMAL;
|
||||||
$player->coin = $coin;
|
$player->coin = $coin;
|
||||||
|
if ($deptId !== null && $deptId > 0) {
|
||||||
|
$player->dept_id = $deptId;
|
||||||
|
}
|
||||||
if ($adminId !== null && $adminId > 0) {
|
if ($adminId !== null && $adminId > 0) {
|
||||||
$player->admin_id = $adminId;
|
$player->admin_id = $adminId;
|
||||||
$adminUser = SystemUser::find($adminId);
|
$adminUser = SystemUser::find($adminId);
|
||||||
if ($adminUser && !empty($adminUser->dept_id)) {
|
if (($deptId === null || $deptId <= 0) && $adminUser && !empty($adminUser->dept_id)) {
|
||||||
$player->dept_id = $adminUser->dept_id;
|
$player->dept_id = $adminUser->dept_id;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -125,6 +181,7 @@ class UserLogic
|
|||||||
]);
|
]);
|
||||||
$token = $tokenResult['access_token'];
|
$token = $tokenResult['access_token'];
|
||||||
UserCache::setSessionByUsername($username, $token);
|
UserCache::setSessionByUsername($username, $token);
|
||||||
|
UserCache::setCurrentUserToken((int) $player->id, $token);
|
||||||
|
|
||||||
$userArr = $player->hidden(['password', 'lottery_config_id', 't1_weight', 't2_weight', 't3_weight', 't4_weight', 't5_weight'])->toArray();
|
$userArr = $player->hidden(['password', 'lottery_config_id', 't1_weight', 't2_weight', 't3_weight', 't4_weight', 't5_weight'])->toArray();
|
||||||
UserCache::setUser((int) $player->id, $userArr);
|
UserCache::setUser((int) $player->id, $userArr);
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ class ApiAccessLogMiddleware implements MiddlewareInterface
|
|||||||
|
|
||||||
/** 请求头名称(小写) */
|
/** 请求头名称(小写) */
|
||||||
private const SENSITIVE_HEADER_NAMES = [
|
private const SENSITIVE_HEADER_NAMES = [
|
||||||
|
'api-key',
|
||||||
'auth-token',
|
'auth-token',
|
||||||
'token',
|
'token',
|
||||||
'authorization',
|
'authorization',
|
||||||
@@ -32,6 +33,8 @@ class ApiAccessLogMiddleware implements MiddlewareInterface
|
|||||||
'secret',
|
'secret',
|
||||||
'signature',
|
'signature',
|
||||||
'token',
|
'token',
|
||||||
|
'api-key',
|
||||||
|
'api_key',
|
||||||
'auth-token',
|
'auth-token',
|
||||||
'auth_token',
|
'auth_token',
|
||||||
'old_token',
|
'old_token',
|
||||||
|
|||||||
64
server/app/api/middleware/ApiKeyMiddleware.php
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace app\api\middleware;
|
||||||
|
|
||||||
|
use app\api\util\ReturnCode;
|
||||||
|
use plugin\saiadmin\exception\ApiException;
|
||||||
|
use Webman\Http\Request;
|
||||||
|
use Webman\Http\Response;
|
||||||
|
use Webman\MiddlewareInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 校验对接平台 api-key(与 .env 中 API_KEY 一致)
|
||||||
|
* 仅用于 /api/v1/* 平台对接接口
|
||||||
|
*
|
||||||
|
* 取值优先级(按顺序读取,首个非空即采用):
|
||||||
|
* 1. 请求头 api-key(推荐)
|
||||||
|
* 2. 查询参数 api_key / api-key
|
||||||
|
* 3. body 表单/JSON api_key / api-key
|
||||||
|
*/
|
||||||
|
class ApiKeyMiddleware implements MiddlewareInterface
|
||||||
|
{
|
||||||
|
public function process(Request $request, callable $handler): Response
|
||||||
|
{
|
||||||
|
$expected = (string) config('api.platform_api_key', '');
|
||||||
|
if ($expected === '') {
|
||||||
|
throw new ApiException('API_KEY is not configured', ReturnCode::SERVER_ERROR);
|
||||||
|
}
|
||||||
|
|
||||||
|
$apiKey = $this->resolveApiKey($request);
|
||||||
|
if ($apiKey === '') {
|
||||||
|
throw new ApiException('Please provide api-key', ReturnCode::UNAUTHORIZED);
|
||||||
|
}
|
||||||
|
if (!hash_equals($expected, $apiKey)) {
|
||||||
|
throw new ApiException('Invalid api-key', ReturnCode::FORBIDDEN);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $handler($request);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function resolveApiKey(Request $request): string
|
||||||
|
{
|
||||||
|
$headerValue = $request->header('api-key');
|
||||||
|
if ($headerValue !== null && trim((string) $headerValue) !== '') {
|
||||||
|
return trim((string) $headerValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (['api_key', 'api-key'] as $key) {
|
||||||
|
$val = $request->get($key);
|
||||||
|
if ($val !== null && trim((string) $val) !== '') {
|
||||||
|
return trim((string) $val);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (['api_key', 'api-key'] as $key) {
|
||||||
|
$val = $request->post($key);
|
||||||
|
if ($val !== null && trim((string) $val) !== '') {
|
||||||
|
return trim((string) $val);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ namespace app\api\middleware;
|
|||||||
|
|
||||||
use app\api\cache\AuthTokenCache;
|
use app\api\cache\AuthTokenCache;
|
||||||
use app\api\util\ReturnCode;
|
use app\api\util\ReturnCode;
|
||||||
|
use plugin\saiadmin\app\model\system\SystemUser;
|
||||||
use plugin\saiadmin\exception\ApiException;
|
use plugin\saiadmin\exception\ApiException;
|
||||||
use Tinywan\Jwt\JwtToken;
|
use Tinywan\Jwt\JwtToken;
|
||||||
use Tinywan\Jwt\Exception\JwtTokenException;
|
use Tinywan\Jwt\Exception\JwtTokenException;
|
||||||
@@ -53,7 +54,17 @@ class AuthTokenMiddleware implements MiddlewareInterface
|
|||||||
throw new ApiException('auth-token invalid or expired', ReturnCode::TOKEN_INVALID);
|
throw new ApiException('auth-token invalid or expired', ReturnCode::TOKEN_INVALID);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$agent = SystemUser::where('agent_id', $agentId)->find();
|
||||||
|
if (!$agent || (int) ($agent->status ?? 0) !== 1) {
|
||||||
|
throw new ApiException('Invalid agent_id', ReturnCode::FORBIDDEN);
|
||||||
|
}
|
||||||
|
if (empty($agent->dept_id) || (int) $agent->dept_id <= 0) {
|
||||||
|
throw new ApiException('Agent channel is not configured', ReturnCode::FORBIDDEN);
|
||||||
|
}
|
||||||
|
|
||||||
$request->agent_id = $agentId;
|
$request->agent_id = $agentId;
|
||||||
|
$request->agent_admin_id = (int) $agent->id;
|
||||||
|
$request->agent_dept_id = (int) $agent->dept_id;
|
||||||
return $handler($request);
|
return $handler($request);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -53,10 +53,14 @@ class TokenMiddleware implements MiddlewareInterface
|
|||||||
if ($username === '') {
|
if ($username === '') {
|
||||||
throw new ApiException('Invalid or expired token', ReturnCode::TOKEN_INVALID);
|
throw new ApiException('Invalid or expired token', ReturnCode::TOKEN_INVALID);
|
||||||
}
|
}
|
||||||
|
$userId = (int) ($extend['id'] ?? 0);
|
||||||
|
if ($userId <= 0) {
|
||||||
|
throw new ApiException('Invalid or expired token', ReturnCode::TOKEN_INVALID);
|
||||||
|
}
|
||||||
|
|
||||||
$currentToken = UserCache::getSessionTokenByUsername($username);
|
$currentToken = UserCache::getCurrentUserToken($userId);
|
||||||
if ($currentToken === null || $currentToken === '') {
|
if ($currentToken === null || $currentToken === '') {
|
||||||
$player = DicePlayer::where('username', $username)->find();
|
$player = DicePlayer::find($userId);
|
||||||
if (!$player) {
|
if (!$player) {
|
||||||
throw new ApiException('Please register', ReturnCode::TOKEN_INVALID);
|
throw new ApiException('Please register', ReturnCode::TOKEN_INVALID);
|
||||||
}
|
}
|
||||||
@@ -68,17 +72,17 @@ class TokenMiddleware implements MiddlewareInterface
|
|||||||
|
|
||||||
// 优先从 Redis 缓存取玩家,避免每次请求都查库
|
// 优先从 Redis 缓存取玩家,避免每次请求都查库
|
||||||
$player = null;
|
$player = null;
|
||||||
$cached = UserCache::getPlayerByUsername($username);
|
$cached = UserCache::getUser($userId);
|
||||||
if ($cached !== null && isset($cached['id'])) {
|
if (!empty($cached) && isset($cached['id']) && (int) $cached['id'] === $userId) {
|
||||||
$player = (new DicePlayer())->data($cached, true);
|
$player = (new DicePlayer())->data($cached, true);
|
||||||
}
|
}
|
||||||
if ($player === null) {
|
if ($player === null) {
|
||||||
$player = DicePlayer::where('username', $username)->find();
|
$player = DicePlayer::find($userId);
|
||||||
if (!$player) {
|
if (!$player) {
|
||||||
UserCache::deleteSessionByUsername($username);
|
UserCache::deleteSessionByUsername($username);
|
||||||
throw new ApiException('Please login again', ReturnCode::TOKEN_INVALID);
|
throw new ApiException('Please login again', ReturnCode::TOKEN_INVALID);
|
||||||
}
|
}
|
||||||
UserCache::setPlayerByUsername($username, $player->hidden(['password'])->toArray());
|
UserCache::setUser($userId, $player->hidden(['password'])->toArray());
|
||||||
}
|
}
|
||||||
$request->player_id = (int) $player->id;
|
$request->player_id = (int) $player->id;
|
||||||
$request->player = $player;
|
$request->player = $player;
|
||||||
|
|||||||
@@ -83,6 +83,47 @@ class LotteryService
|
|||||||
Cache::set($key, json_encode($data), self::EXPIRE);
|
Cache::set($key, json_encode($data), self::EXPIRE);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 使玩家彩金池 Redis 快照失效(下次 getOrCreate 从库重建)
|
||||||
|
*/
|
||||||
|
public static function invalidatePlayerLotteryCache(int $playerId): void
|
||||||
|
{
|
||||||
|
if ($playerId <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Cache::delete(self::getRedisKey($playerId));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 若 Redis 中已有该玩家彩金池快照,则仅更新 player_weights(避免全量重建)
|
||||||
|
*
|
||||||
|
* @param array{t1_weight?:int,t2_weight?:int,t3_weight?:int,t4_weight?:int,t5_weight?:int} $weights
|
||||||
|
*/
|
||||||
|
public static function patchPlayerWeightsCache(int $playerId, array $weights): void
|
||||||
|
{
|
||||||
|
if ($playerId <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$key = self::getRedisKey($playerId);
|
||||||
|
$cached = Cache::get($key);
|
||||||
|
if (!$cached || !is_string($cached)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$data = json_decode($cached, true);
|
||||||
|
if (!is_array($data)) {
|
||||||
|
Cache::delete($key);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$data['player_weights'] = [
|
||||||
|
't1_weight' => (int) ($weights['t1_weight'] ?? 0),
|
||||||
|
't2_weight' => (int) ($weights['t2_weight'] ?? 0),
|
||||||
|
't3_weight' => (int) ($weights['t3_weight'] ?? 0),
|
||||||
|
't4_weight' => (int) ($weights['t4_weight'] ?? 0),
|
||||||
|
't5_weight' => (int) ($weights['t5_weight'] ?? 0),
|
||||||
|
];
|
||||||
|
Cache::set($key, json_encode($data), self::EXPIRE);
|
||||||
|
}
|
||||||
|
|
||||||
/** 根据奖池配置的 t1_weight..t5_weight 权重随机抽取档位 T1-T5 */
|
/** 根据奖池配置的 t1_weight..t5_weight 权重随机抽取档位 T1-T5 */
|
||||||
public static function drawTierByWeights(DiceLotteryPoolConfig $config): string
|
public static function drawTierByWeights(DiceLotteryPoolConfig $config): string
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -25,16 +25,13 @@ class DiceDashboardController extends BaseController
|
|||||||
#[Permission('工作台数据统计', 'core:console:list')]
|
#[Permission('工作台数据统计', 'core:console:list')]
|
||||||
public function statistics(Request $request): Response
|
public function statistics(Request $request): Response
|
||||||
{
|
{
|
||||||
$thisWeekStart = date('Y-m-d 00:00:00', strtotime('monday this week'));
|
[$thisStart, $thisEnd, $lastStart, $lastEnd] = $this->resolveDateRanges($request);
|
||||||
$thisWeekEnd = date('Y-m-d 23:59:59', strtotime('sunday this week'));
|
|
||||||
$lastWeekStart = date('Y-m-d 00:00:00', strtotime('monday last week'));
|
|
||||||
$lastWeekEnd = date('Y-m-d 23:59:59', strtotime('sunday last week'));
|
|
||||||
|
|
||||||
$adminInfo = $this->adminInfo ?? null;
|
$adminInfo = $this->adminInfo ?? null;
|
||||||
$filterDeptId = $request->input('dept_id');
|
$filterDeptId = $request->input('dept_id');
|
||||||
|
|
||||||
$playerQueryThis = DicePlayer::whereBetween('create_time', [$thisWeekStart, $thisWeekEnd]);
|
$playerQueryThis = DicePlayer::whereBetween('create_time', [$thisStart, $thisEnd]);
|
||||||
$playerQueryLast = DicePlayer::whereBetween('create_time', [$lastWeekStart, $lastWeekEnd]);
|
$playerQueryLast = DicePlayer::whereBetween('create_time', [$lastStart, $lastEnd]);
|
||||||
AdminScopeHelper::applyAdminScope($playerQueryThis, $adminInfo, $filterDeptId);
|
AdminScopeHelper::applyAdminScope($playerQueryThis, $adminInfo, $filterDeptId);
|
||||||
AdminScopeHelper::applyAdminScope($playerQueryLast, $adminInfo, $filterDeptId);
|
AdminScopeHelper::applyAdminScope($playerQueryLast, $adminInfo, $filterDeptId);
|
||||||
$playerThis = $playerQueryThis->count();
|
$playerThis = $playerQueryThis->count();
|
||||||
@@ -42,35 +39,35 @@ class DiceDashboardController extends BaseController
|
|||||||
|
|
||||||
$chargeQueryThis = DicePlayerWalletRecord::where('type', 0)
|
$chargeQueryThis = DicePlayerWalletRecord::where('type', 0)
|
||||||
->where('coin', '>', 0)
|
->where('coin', '>', 0)
|
||||||
->whereBetween('create_time', [$thisWeekStart, $thisWeekEnd]);
|
->whereBetween('create_time', [$thisStart, $thisEnd]);
|
||||||
$chargeQueryLast = DicePlayerWalletRecord::where('type', 0)
|
$chargeQueryLast = DicePlayerWalletRecord::where('type', 0)
|
||||||
->where('coin', '>', 0)
|
->where('coin', '>', 0)
|
||||||
->whereBetween('create_time', [$lastWeekStart, $lastWeekEnd]);
|
->whereBetween('create_time', [$lastStart, $lastEnd]);
|
||||||
AdminScopeHelper::applyAdminScope($chargeQueryThis, $adminInfo, $filterDeptId);
|
AdminScopeHelper::applyAdminScope($chargeQueryThis, $adminInfo, $filterDeptId);
|
||||||
AdminScopeHelper::applyAdminScope($chargeQueryLast, $adminInfo, $filterDeptId);
|
AdminScopeHelper::applyAdminScope($chargeQueryLast, $adminInfo, $filterDeptId);
|
||||||
$chargeThis = $chargeQueryThis->sum('coin');
|
$chargeThis = $chargeQueryThis->sum('coin');
|
||||||
$chargeLast = $chargeQueryLast->sum('coin');
|
$chargeLast = $chargeQueryLast->sum('coin');
|
||||||
|
|
||||||
$withdrawQueryThis = DicePlayerWalletRecord::where('type', 1)
|
$withdrawQueryThis = DicePlayerWalletRecord::where('type', 1)
|
||||||
->whereBetween('create_time', [$thisWeekStart, $thisWeekEnd]);
|
->whereBetween('create_time', [$thisStart, $thisEnd]);
|
||||||
$withdrawQueryLast = DicePlayerWalletRecord::where('type', 1)
|
$withdrawQueryLast = DicePlayerWalletRecord::where('type', 1)
|
||||||
->whereBetween('create_time', [$lastWeekStart, $lastWeekEnd]);
|
->whereBetween('create_time', [$lastStart, $lastEnd]);
|
||||||
AdminScopeHelper::applyAdminScope($withdrawQueryThis, $adminInfo, $filterDeptId);
|
AdminScopeHelper::applyAdminScope($withdrawQueryThis, $adminInfo, $filterDeptId);
|
||||||
AdminScopeHelper::applyAdminScope($withdrawQueryLast, $adminInfo, $filterDeptId);
|
AdminScopeHelper::applyAdminScope($withdrawQueryLast, $adminInfo, $filterDeptId);
|
||||||
$withdrawThis = $withdrawQueryThis->sum(Db::raw('ABS(coin)'));
|
$withdrawThis = $withdrawQueryThis->sum(Db::raw('ABS(coin)'));
|
||||||
$withdrawLast = $withdrawQueryLast->sum(Db::raw('ABS(coin)'));
|
$withdrawLast = $withdrawQueryLast->sum(Db::raw('ABS(coin)'));
|
||||||
|
|
||||||
$playQueryThis = DicePlayRecord::whereBetween('create_time', [$thisWeekStart, $thisWeekEnd]);
|
$playQueryThis = DicePlayRecord::whereBetween('create_time', [$thisStart, $thisEnd]);
|
||||||
$playQueryLast = DicePlayRecord::whereBetween('create_time', [$lastWeekStart, $lastWeekEnd]);
|
$playQueryLast = DicePlayRecord::whereBetween('create_time', [$lastStart, $lastEnd]);
|
||||||
AdminScopeHelper::applyAdminScope($playQueryThis, $adminInfo, $filterDeptId);
|
AdminScopeHelper::applyAdminScope($playQueryThis, $adminInfo, $filterDeptId);
|
||||||
AdminScopeHelper::applyAdminScope($playQueryLast, $adminInfo, $filterDeptId);
|
AdminScopeHelper::applyAdminScope($playQueryLast, $adminInfo, $filterDeptId);
|
||||||
$playThis = $playQueryThis->count();
|
$playThis = $playQueryThis->count();
|
||||||
$playLast = $playQueryLast->count();
|
$playLast = $playQueryLast->count();
|
||||||
|
|
||||||
$playerChange = $this->calcWeekChange($playerThis, $playerLast);
|
$playerChange = $this->calcPeriodChange($playerThis, $playerLast);
|
||||||
$chargeChange = $this->calcWeekChange((float) $chargeThis, (float) $chargeLast);
|
$chargeChange = $this->calcPeriodChange((float) $chargeThis, (float) $chargeLast);
|
||||||
$withdrawChange = $this->calcWeekChange((float) $withdrawThis, (float) $withdrawLast);
|
$withdrawChange = $this->calcPeriodChange((float) $withdrawThis, (float) $withdrawLast);
|
||||||
$playChange = $this->calcWeekChange($playThis, $playLast);
|
$playChange = $this->calcPeriodChange($playThis, $playLast);
|
||||||
|
|
||||||
return $this->success([
|
return $this->success([
|
||||||
'player_count' => $playerThis,
|
'player_count' => $playerThis,
|
||||||
@@ -81,6 +78,7 @@ class DiceDashboardController extends BaseController
|
|||||||
'withdraw_amount_change' => $withdrawChange,
|
'withdraw_amount_change' => $withdrawChange,
|
||||||
'play_count' => $playThis,
|
'play_count' => $playThis,
|
||||||
'play_count_change' => $playChange,
|
'play_count_change' => $playChange,
|
||||||
|
'date' => $this->resolveRequestDate($request),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -179,8 +177,9 @@ class DiceDashboardController extends BaseController
|
|||||||
$q->field('id,username');
|
$q->field('id,username');
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
->where('type', 0)
|
->where('type', 0);
|
||||||
->order('create_time', 'desc')
|
$this->applyDashboardDateFilter($query, $request);
|
||||||
|
$query->order('create_time', 'desc')
|
||||||
->limit(50);
|
->limit(50);
|
||||||
AdminScopeHelper::applyAdminScope($query, $adminInfo, $request->input('dept_id'));
|
AdminScopeHelper::applyAdminScope($query, $adminInfo, $request->input('dept_id'));
|
||||||
$list = $query->select();
|
$list = $query->select();
|
||||||
@@ -204,8 +203,9 @@ class DiceDashboardController extends BaseController
|
|||||||
public function newPlayerList(Request $request): Response
|
public function newPlayerList(Request $request): Response
|
||||||
{
|
{
|
||||||
$adminInfo = $this->adminInfo ?? null;
|
$adminInfo = $this->adminInfo ?? null;
|
||||||
$query = DicePlayer::field('username,coin,total_ticket_count,create_time')
|
$query = DicePlayer::field('username,coin,total_ticket_count,create_time');
|
||||||
->order('create_time', 'desc')
|
$this->applyDashboardDateFilter($query, $request);
|
||||||
|
$query->order('create_time', 'desc')
|
||||||
->limit(50);
|
->limit(50);
|
||||||
AdminScopeHelper::applyAdminScope($query, $adminInfo, $request->input('dept_id'));
|
AdminScopeHelper::applyAdminScope($query, $adminInfo, $request->input('dept_id'));
|
||||||
$list = $query->select();
|
$list = $query->select();
|
||||||
@@ -235,8 +235,9 @@ class DiceDashboardController extends BaseController
|
|||||||
},
|
},
|
||||||
])
|
])
|
||||||
->where('status', 1)
|
->where('status', 1)
|
||||||
->field('id,player_id,reward_tier,win_coin,create_time')
|
->field('id,player_id,reward_tier,win_coin,create_time');
|
||||||
->order('create_time', 'desc')
|
$this->applyDashboardDateFilter($query, $request);
|
||||||
|
$query->order('create_time', 'desc')
|
||||||
->limit(50);
|
->limit(50);
|
||||||
AdminScopeHelper::applyAdminScope($query, $adminInfo, $request->input('dept_id'));
|
AdminScopeHelper::applyAdminScope($query, $adminInfo, $request->input('dept_id'));
|
||||||
$list = $query->select();
|
$list = $query->select();
|
||||||
@@ -275,7 +276,7 @@ class DiceDashboardController extends BaseController
|
|||||||
return $labels;
|
return $labels;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function calcWeekChange($current, $last): float
|
private function calcPeriodChange($current, $last): float
|
||||||
{
|
{
|
||||||
if ($last == 0) {
|
if ($last == 0) {
|
||||||
return $current > 0 ? 100.0 : 0.0;
|
return $current > 0 ? 100.0 : 0.0;
|
||||||
@@ -283,6 +284,56 @@ class DiceDashboardController extends BaseController
|
|||||||
return round((($current - $last) / $last) * 100, 1);
|
return round((($current - $last) / $last) * 100, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析工作台统计区间:有 date 按单日对比昨日,无 date 按本周对比上周
|
||||||
|
*
|
||||||
|
* @return array{0: string, 1: string, 2: string, 3: string}
|
||||||
|
*/
|
||||||
|
private function resolveDateRanges(Request $request): array
|
||||||
|
{
|
||||||
|
$date = $this->resolveRequestDate($request);
|
||||||
|
if ($date === '') {
|
||||||
|
return [
|
||||||
|
date('Y-m-d 00:00:00', strtotime('monday this week')),
|
||||||
|
date('Y-m-d 23:59:59', strtotime('sunday this week')),
|
||||||
|
date('Y-m-d 00:00:00', strtotime('monday last week')),
|
||||||
|
date('Y-m-d 23:59:59', strtotime('sunday last week')),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$lastDate = date('Y-m-d', strtotime($date . ' -1 day'));
|
||||||
|
|
||||||
|
return [
|
||||||
|
$date . ' 00:00:00',
|
||||||
|
$date . ' 23:59:59',
|
||||||
|
$lastDate . ' 00:00:00',
|
||||||
|
$lastDate . ' 23:59:59',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function resolveRequestDate(Request $request): string
|
||||||
|
{
|
||||||
|
$date = trim((string) $request->input('date', ''));
|
||||||
|
if ($date === '' || strtotime($date) === false) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return date('Y-m-d', strtotime($date));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param mixed $query
|
||||||
|
*/
|
||||||
|
private function applyDashboardDateFilter($query, Request $request, string $column = 'create_time'): void
|
||||||
|
{
|
||||||
|
$date = $this->resolveRequestDate($request);
|
||||||
|
if ($date === '') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$query->whereBetween($column, [$date . ' 00:00:00', $date . ' 23:59:59']);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 钱包流水 SQL 渠道条件;非超管无渠道时返回 __empty__
|
* 钱包流水 SQL 渠道条件;非超管无渠道时返回 __empty__
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -38,19 +38,23 @@ class DiceLotteryPoolConfigController extends BaseController
|
|||||||
#[Permission('色子奖池配置列表', 'dice:lottery_pool_config:index:index')]
|
#[Permission('色子奖池配置列表', 'dice:lottery_pool_config:index:index')]
|
||||||
public function getOptions(Request $request): Response
|
public function getOptions(Request $request): Response
|
||||||
{
|
{
|
||||||
$query = DiceLotteryPoolConfig::field('id,name,t1_weight,t2_weight,t3_weight,t4_weight,t5_weight')
|
$query = DiceLotteryPoolConfig::field('id,name,remark,t1_weight,t2_weight,t3_weight,t4_weight,t5_weight')
|
||||||
->order('id', 'asc');
|
->order('id', 'asc');
|
||||||
AdminScopeHelper::applyConfigScope($query, $this->adminInfo ?? null, $request->input('dept_id'));
|
AdminScopeHelper::applyConfigScope($query, $this->adminInfo ?? null, $request->input('dept_id'));
|
||||||
$list = $query->select();
|
$list = $query->select();
|
||||||
$data = $list->map(function ($item) {
|
$data = $list->map(function ($item) {
|
||||||
|
$row = is_array($item) ? $item : $item->toArray();
|
||||||
|
$display = DiceLotteryPoolConfig::displayLabel($row);
|
||||||
return [
|
return [
|
||||||
'id' => (int) $item['id'],
|
'id' => (int) ($row['id'] ?? 0),
|
||||||
'name' => (string) ($item['name'] ?? ''),
|
'name' => (string) ($row['name'] ?? ''),
|
||||||
't1_weight' => (int) ($item['t1_weight'] ?? 0),
|
'remark' => (string) ($row['remark'] ?? ''),
|
||||||
't2_weight' => (int) ($item['t2_weight'] ?? 0),
|
'display_name' => $display,
|
||||||
't3_weight' => (int) ($item['t3_weight'] ?? 0),
|
't1_weight' => (int) ($row['t1_weight'] ?? 0),
|
||||||
't4_weight' => (int) ($item['t4_weight'] ?? 0),
|
't2_weight' => (int) ($row['t2_weight'] ?? 0),
|
||||||
't5_weight' => (int) ($item['t5_weight'] ?? 0),
|
't3_weight' => (int) ($row['t3_weight'] ?? 0),
|
||||||
|
't4_weight' => (int) ($row['t4_weight'] ?? 0),
|
||||||
|
't5_weight' => (int) ($row['t5_weight'] ?? 0),
|
||||||
];
|
];
|
||||||
})->toArray();
|
})->toArray();
|
||||||
return $this->success($data);
|
return $this->success($data);
|
||||||
|
|||||||
@@ -51,6 +51,8 @@ class DicePlayRecordController extends BaseController
|
|||||||
['reward_ui_text', ''],
|
['reward_ui_text', ''],
|
||||||
['reward_tier', ''],
|
['reward_tier', ''],
|
||||||
['direction', ''],
|
['direction', ''],
|
||||||
|
['create_time_min', ''],
|
||||||
|
['create_time_max', ''],
|
||||||
]);
|
]);
|
||||||
$query = $this->logic->search($where);
|
$query = $this->logic->search($where);
|
||||||
AdminScopeHelper::applyAdminScope($query, $this->adminInfo ?? null, $request->input('dept_id'));
|
AdminScopeHelper::applyAdminScope($query, $this->adminInfo ?? null, $request->input('dept_id'));
|
||||||
@@ -92,7 +94,7 @@ class DicePlayRecordController extends BaseController
|
|||||||
#[Permission('玩家抽奖记录列表', 'dice:play_record:index:index')]
|
#[Permission('玩家抽奖记录列表', 'dice:play_record:index:index')]
|
||||||
public function getLotteryConfigOptions(Request $request): Response
|
public function getLotteryConfigOptions(Request $request): Response
|
||||||
{
|
{
|
||||||
$query = DiceLotteryPoolConfig::field('id,name')->order('id', 'asc');
|
$query = DiceLotteryPoolConfig::field('id,name,remark')->order('id', 'asc');
|
||||||
$requestDeptId = AdminScopeHelper::pickRequestDeptId(
|
$requestDeptId = AdminScopeHelper::pickRequestDeptId(
|
||||||
$request->input('dept_id'),
|
$request->input('dept_id'),
|
||||||
$request->all()
|
$request->all()
|
||||||
@@ -100,7 +102,13 @@ class DicePlayRecordController extends BaseController
|
|||||||
AdminScopeHelper::applyConfigScope($query, $this->adminInfo ?? null, $requestDeptId);
|
AdminScopeHelper::applyConfigScope($query, $this->adminInfo ?? null, $requestDeptId);
|
||||||
$list = $query->select();
|
$list = $query->select();
|
||||||
$data = $list->map(function ($item) {
|
$data = $list->map(function ($item) {
|
||||||
return ['id' => (int) $item['id'], 'name' => (string) ($item['name'] ?? '')];
|
$row = is_array($item) ? $item : $item->toArray();
|
||||||
|
return [
|
||||||
|
'id' => (int) ($row['id'] ?? 0),
|
||||||
|
'name' => (string) ($row['name'] ?? ''),
|
||||||
|
'remark' => (string) ($row['remark'] ?? ''),
|
||||||
|
'display_name' => DiceLotteryPoolConfig::displayLabel($row),
|
||||||
|
];
|
||||||
})->toArray();
|
})->toArray();
|
||||||
return $this->success($data);
|
return $this->success($data);
|
||||||
}
|
}
|
||||||
@@ -144,31 +152,6 @@ class DicePlayRecordController extends BaseController
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 更新数据
|
|
||||||
* @param Request $request
|
|
||||||
* @return Response
|
|
||||||
*/
|
|
||||||
#[Permission('玩家抽奖记录修改', 'dice:play_record:index:update')]
|
|
||||||
public function update(Request $request): Response
|
|
||||||
{
|
|
||||||
$data = $request->post();
|
|
||||||
$this->validate('update', $data);
|
|
||||||
$model = $this->logic->read($data['id'] ?? 0);
|
|
||||||
if ($model) {
|
|
||||||
$recordDeptId = is_array($model) ? ($model['dept_id'] ?? null) : ($model->dept_id ?? null);
|
|
||||||
if (! AdminScopeHelper::canAccessDept($this->adminInfo ?? null, $recordDeptId, $request->input('dept_id'))) {
|
|
||||||
return $this->fail('no permission to update this record');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
$result = $this->logic->edit($data['id'], $data);
|
|
||||||
if ($result) {
|
|
||||||
return $this->success('update success');
|
|
||||||
} else {
|
|
||||||
return $this->fail('update failed');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 删除数据
|
* 删除数据
|
||||||
* @param Request $request
|
* @param Request $request
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ class DicePlayRecordTestController extends BaseController
|
|||||||
{
|
{
|
||||||
$where = $request->more([
|
$where = $request->more([
|
||||||
['reward_config_record_id', ''],
|
['reward_config_record_id', ''],
|
||||||
|
['lottery_config_id', ''],
|
||||||
['lottery_type', ''],
|
['lottery_type', ''],
|
||||||
['direction', ''],
|
['direction', ''],
|
||||||
['is_win', ''],
|
['is_win', ''],
|
||||||
@@ -103,31 +104,6 @@ class DicePlayRecordTestController extends BaseController
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 更新数据
|
|
||||||
* @param Request $request
|
|
||||||
* @return Response
|
|
||||||
*/
|
|
||||||
#[Permission('玩家抽奖记录(测试数据)修改', 'dice:play_record_test:index:update')]
|
|
||||||
public function update(Request $request): Response
|
|
||||||
{
|
|
||||||
$data = $request->post();
|
|
||||||
$this->validate('update', $data);
|
|
||||||
$model = $this->logic->read($data['id'] ?? 0);
|
|
||||||
if ($model) {
|
|
||||||
$recordDeptId = is_array($model) ? ($model['dept_id'] ?? null) : ($model->dept_id ?? null);
|
|
||||||
if (! AdminScopeHelper::canAccessDept($this->adminInfo ?? null, $recordDeptId, $request->input('dept_id'))) {
|
|
||||||
return $this->fail('no permission to update this record');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
$result = $this->logic->edit($data['id'], $data);
|
|
||||||
if ($result) {
|
|
||||||
return $this->success('update success');
|
|
||||||
} else {
|
|
||||||
return $this->fail('update failed');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 删除数据
|
* 删除数据
|
||||||
* @param Request $request
|
* @param Request $request
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ class DicePlayerController extends BaseController
|
|||||||
#[Permission('玩家列表', 'dice:player:index:index')]
|
#[Permission('玩家列表', 'dice:player:index:index')]
|
||||||
public function getLotteryConfigOptions(Request $request): Response
|
public function getLotteryConfigOptions(Request $request): Response
|
||||||
{
|
{
|
||||||
$query = DiceLotteryPoolConfig::field('id,name')->order('id', 'asc');
|
$query = DiceLotteryPoolConfig::field('id,name,remark')->order('id', 'asc');
|
||||||
$requestDeptId = AdminScopeHelper::pickRequestDeptId(
|
$requestDeptId = AdminScopeHelper::pickRequestDeptId(
|
||||||
$request->input('dept_id'),
|
$request->input('dept_id'),
|
||||||
$request->all()
|
$request->all()
|
||||||
@@ -51,7 +51,13 @@ class DicePlayerController extends BaseController
|
|||||||
AdminScopeHelper::applyConfigScope($query, $this->adminInfo ?? null, $requestDeptId);
|
AdminScopeHelper::applyConfigScope($query, $this->adminInfo ?? null, $requestDeptId);
|
||||||
$list = $query->select();
|
$list = $query->select();
|
||||||
$data = $list->map(function ($item) {
|
$data = $list->map(function ($item) {
|
||||||
return ['id' => (int) $item['id'], 'name' => (string) ($item['name'] ?? '')];
|
$row = is_array($item) ? $item : $item->toArray();
|
||||||
|
return [
|
||||||
|
'id' => (int) ($row['id'] ?? 0),
|
||||||
|
'name' => (string) ($row['name'] ?? ''),
|
||||||
|
'remark' => (string) ($row['remark'] ?? ''),
|
||||||
|
'display_name' => DiceLotteryPoolConfig::displayLabel($row),
|
||||||
|
];
|
||||||
})->toArray();
|
})->toArray();
|
||||||
return $this->success($data);
|
return $this->success($data);
|
||||||
}
|
}
|
||||||
@@ -181,6 +187,8 @@ class DicePlayerController extends BaseController
|
|||||||
['status', ''],
|
['status', ''],
|
||||||
['coin', ''],
|
['coin', ''],
|
||||||
['lottery_config_id', ''],
|
['lottery_config_id', ''],
|
||||||
|
['create_time_min', ''],
|
||||||
|
['create_time_max', ''],
|
||||||
]);
|
]);
|
||||||
$query = $this->logic->search($where);
|
$query = $this->logic->search($where);
|
||||||
AdminScopeHelper::applyAdminScope($query, $this->adminInfo ?? null, $request->input('dept_id'));
|
AdminScopeHelper::applyAdminScope($query, $this->adminInfo ?? null, $request->input('dept_id'));
|
||||||
@@ -355,6 +363,7 @@ class DicePlayerController extends BaseController
|
|||||||
return $this->fail('generate token failed');
|
return $this->fail('generate token failed');
|
||||||
}
|
}
|
||||||
UserCache::setSessionByUsername($username, $token);
|
UserCache::setSessionByUsername($username, $token);
|
||||||
|
UserCache::setCurrentUserToken((int) $player->id, $token);
|
||||||
$userArr = $player->hidden(['password', 'lottery_config_id', 't1_weight', 't2_weight', 't3_weight', 't4_weight', 't5_weight'])->toArray();
|
$userArr = $player->hidden(['password', 'lottery_config_id', 't1_weight', 't2_weight', 't3_weight', 't4_weight', 't5_weight'])->toArray();
|
||||||
UserCache::setUser((int) $player->id, $userArr);
|
UserCache::setUser((int) $player->id, $userArr);
|
||||||
UserCache::setPlayerByUsername($username, $userArr);
|
UserCache::setPlayerByUsername($username, $userArr);
|
||||||
@@ -363,6 +372,9 @@ class DicePlayerController extends BaseController
|
|||||||
if ($gameUrlBase === '') {
|
if ($gameUrlBase === '') {
|
||||||
return $this->fail('GAME_URL is not configured');
|
return $this->fail('GAME_URL is not configured');
|
||||||
}
|
}
|
||||||
|
if (!str_starts_with($gameUrlBase, 'http://') && !str_starts_with($gameUrlBase, 'https://')) {
|
||||||
|
$gameUrlBase = 'https://' . $gameUrlBase;
|
||||||
|
}
|
||||||
$tokenInUrl = str_replace('%3D', '=', urlencode($token));
|
$tokenInUrl = str_replace('%3D', '=', urlencode($token));
|
||||||
$url = $gameUrlBase . '/?token=' . $tokenInUrl . '&lang=' . $lang;
|
$url = $gameUrlBase . '/?token=' . $tokenInUrl . '&lang=' . $lang;
|
||||||
return $this->success(['url' => $url]);
|
return $this->success(['url' => $url]);
|
||||||
|
|||||||
@@ -117,31 +117,6 @@ class DicePlayerTicketRecordController extends BaseController
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 更新数据
|
|
||||||
* @param Request $request
|
|
||||||
* @return Response
|
|
||||||
*/
|
|
||||||
#[Permission('抽奖券获取记录修改', 'dice:player_ticket_record:index:update')]
|
|
||||||
public function update(Request $request): Response
|
|
||||||
{
|
|
||||||
$data = $request->post();
|
|
||||||
$this->validate('update', $data);
|
|
||||||
$model = $this->logic->read($data['id'] ?? 0);
|
|
||||||
if ($model) {
|
|
||||||
$recordDeptId = is_array($model) ? ($model['dept_id'] ?? null) : ($model->dept_id ?? null);
|
|
||||||
if (! AdminScopeHelper::canAccessDept($this->adminInfo ?? null, $recordDeptId, $request->input('dept_id'))) {
|
|
||||||
return $this->fail('no permission to update this record');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
$result = $this->logic->edit($data['id'], $data);
|
|
||||||
if ($result) {
|
|
||||||
return $this->success('update success');
|
|
||||||
} else {
|
|
||||||
return $this->fail('update failed');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 删除数据
|
* 删除数据
|
||||||
* @param Request $request
|
* @param Request $request
|
||||||
|
|||||||
@@ -48,10 +48,18 @@ class DicePlayerWalletRecordController extends BaseController
|
|||||||
['create_time_max', ''],
|
['create_time_max', ''],
|
||||||
]);
|
]);
|
||||||
$deptId = $request->input('dept_id');
|
$deptId = $request->input('dept_id');
|
||||||
$totalCoinChange = $this->logic->sumCoinBySearch($where, $this->adminInfo ?? null, $deptId);
|
|
||||||
|
|
||||||
$query = $this->logic->search($where);
|
$query = $this->logic->search($where);
|
||||||
AdminScopeHelper::applyAdminScope($query, $this->adminInfo ?? null, $deptId);
|
AdminScopeHelper::applyAdminScope($query, $this->adminInfo ?? null, $deptId);
|
||||||
|
|
||||||
|
$sumQuery = clone $query;
|
||||||
|
$totalCoinChange = round((float) $sumQuery->sum('coin'), 2);
|
||||||
|
|
||||||
|
$inflowQuery = clone $query;
|
||||||
|
$totalCoinInflow = round((float) $inflowQuery->where('coin', '>', 0)->sum('coin'), 2);
|
||||||
|
|
||||||
|
$outflowQuery = clone $query;
|
||||||
|
$totalCoinOutflow = round((float) $outflowQuery->where('coin', '<', 0)->sum('coin'), 2);
|
||||||
|
|
||||||
$query->with([
|
$query->with([
|
||||||
'dicePlayer',
|
'dicePlayer',
|
||||||
'operator',
|
'operator',
|
||||||
@@ -59,6 +67,8 @@ class DicePlayerWalletRecordController extends BaseController
|
|||||||
|
|
||||||
$data = $this->logic->getList($query);
|
$data = $this->logic->getList($query);
|
||||||
$data['total_coin_change'] = $totalCoinChange;
|
$data['total_coin_change'] = $totalCoinChange;
|
||||||
|
$data['total_coin_inflow'] = $totalCoinInflow;
|
||||||
|
$data['total_coin_outflow'] = $totalCoinOutflow;
|
||||||
return $this->success($data);
|
return $this->success($data);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -200,28 +210,4 @@ class DicePlayerWalletRecordController extends BaseController
|
|||||||
return $this->fail('add failed');
|
return $this->fail('add failed');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 更新数据
|
|
||||||
* @param Request $request
|
|
||||||
* @return Response
|
|
||||||
*/
|
|
||||||
#[Permission('玩家钱包流水修改', 'dice:player_wallet_record:index:update')]
|
|
||||||
public function update(Request $request): Response
|
|
||||||
{
|
|
||||||
$data = $request->post();
|
|
||||||
$this->validate('update', $data);
|
|
||||||
$model = $this->logic->read($data['id'] ?? 0);
|
|
||||||
if ($model) {
|
|
||||||
$recordDeptId = is_array($model) ? ($model['dept_id'] ?? null) : ($model->dept_id ?? null);
|
|
||||||
if (! AdminScopeHelper::canAccessDept($this->adminInfo ?? null, $recordDeptId, $request->input('dept_id'))) {
|
|
||||||
return $this->fail('no permission to update this record');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
$result = $this->logic->edit($data['id'], $data);
|
|
||||||
if ($result) {
|
|
||||||
return $this->success('update success');
|
|
||||||
} else {
|
|
||||||
return $this->fail('update failed');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -96,7 +96,7 @@ class DiceRewardController extends BaseController
|
|||||||
* 参数:lottery_config_id 可选;paid_tier_weights / free_tier_weights 自定义档位;
|
* 参数:lottery_config_id 可选;paid_tier_weights / free_tier_weights 自定义档位;
|
||||||
* paid_s_count, paid_n_count
|
* paid_s_count, paid_n_count
|
||||||
* chain_free_mode=1:仅按付费次数模拟;付费抽到再来一次/T5 则在队列中插入免费局(同底注、lottery_type=免费、paid_amount=0)
|
* chain_free_mode=1:仅按付费次数模拟;付费抽到再来一次/T5 则在队列中插入免费局(同底注、lottery_type=免费、paid_amount=0)
|
||||||
* kill_mode_enabled=1:测试内启用杀分;当模拟玩家累计盈利达到 test_safety_line 后,付费抽奖切到 killScore
|
* kill_mode_enabled=1:测试内启用杀分;当模拟池盈利达到 test_safety_line 后,付费抽奖切到 killScore
|
||||||
*/
|
*/
|
||||||
#[Permission('一键测试权重', 'dice:reward:index:startWeightTest')]
|
#[Permission('一键测试权重', 'dice:reward:index:startWeightTest')]
|
||||||
public function startWeightTest(Request $request): Response
|
public function startWeightTest(Request $request): Response
|
||||||
@@ -116,6 +116,7 @@ class DiceRewardController extends BaseController
|
|||||||
'test_safety_line' => $post['test_safety_line'] ?? null,
|
'test_safety_line' => $post['test_safety_line'] ?? null,
|
||||||
'dept_id' => $post['dept_id'] ?? null,
|
'dept_id' => $post['dept_id'] ?? null,
|
||||||
'ante_config_id' => $post['ante_config_id'] ?? null,
|
'ante_config_id' => $post['ante_config_id'] ?? null,
|
||||||
|
'ante_random' => $post['ante_random'] ?? null,
|
||||||
];
|
];
|
||||||
$adminId = isset($this->adminInfo['id']) ? (int) $this->adminInfo['id'] : null;
|
$adminId = isset($this->adminInfo['id']) ? (int) $this->adminInfo['id'] : null;
|
||||||
$requestDeptId = AdminScopeHelper::pickRequestDeptId($request->input('dept_id'), $post);
|
$requestDeptId = AdminScopeHelper::pickRequestDeptId($request->input('dept_id'), $post);
|
||||||
|
|||||||
@@ -127,6 +127,25 @@ class DiceRewardConfigController extends BaseController
|
|||||||
*/
|
*/
|
||||||
#[Permission('修改奖励索引', 'dice:reward_config:index:batchUpdate')]
|
#[Permission('修改奖励索引', 'dice:reward_config:index:batchUpdate')]
|
||||||
public function batchUpdate(Request $request): Response
|
public function batchUpdate(Request $request): Response
|
||||||
|
{
|
||||||
|
return $this->doBatchUpdateIndex($request);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按规则生成并保存奖励索引(需档位结算推荐配置权限,与 batchUpdate 写入逻辑相同)
|
||||||
|
*
|
||||||
|
* @param Request $request items: [{ id, grid_number?, ui_text?, ui_text_en?, real_ev?, tier?, remark? }, ...]
|
||||||
|
*/
|
||||||
|
#[Permission('档位结算推荐配置', 'dice:reward_config:index:tierRecommend')]
|
||||||
|
public function generateIndexByRules(Request $request): Response
|
||||||
|
{
|
||||||
|
return $this->doBatchUpdateIndex($request);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Response
|
||||||
|
*/
|
||||||
|
private function doBatchUpdateIndex(Request $request): Response
|
||||||
{
|
{
|
||||||
$items = $request->post('items', []);
|
$items = $request->post('items', []);
|
||||||
if (! is_array($items)) {
|
if (! is_array($items)) {
|
||||||
@@ -244,6 +263,23 @@ class DiceRewardConfigController extends BaseController
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建奖励对照(预览):不写入 dice_reward,仅计算并返回预览分组数据。
|
||||||
|
* 若当前 dice_reward 与计算结果一致,则 unchanged=true,并在预览中复用现有权重(导入时仍沿用旧权重)。
|
||||||
|
*/
|
||||||
|
#[Permission('创建奖励对照', 'dice:reward_config:index:createRewardReference')]
|
||||||
|
public function createRewardReferencePreview(Request $request): Response
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$rewardLogic = new DiceRewardLogic();
|
||||||
|
$deptId = AdminScopeHelper::resolveConfigDeptId($this->adminInfo ?? null, $request->input('dept_id'));
|
||||||
|
$result = $rewardLogic->createRewardReferencePreviewFromConfig($deptId);
|
||||||
|
return $this->success($result, 'preview reward mapping success');
|
||||||
|
} catch (\plugin\saiadmin\exception\ApiException $e) {
|
||||||
|
return $this->fail($e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 权重配比测试:仅模拟落点统计,不创建游玩记录。按当前配置在内存中模拟 N 次抽奖,返回各 grid_number 落点次数,可选保存到 dice_reward_config_record。
|
* 权重配比测试:仅模拟落点统计,不创建游玩记录。按当前配置在内存中模拟 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 概率
|
* @param Request $request test_count: 100|500|1000, save_record: bool, lottery_config_id: int|null 奖池配置ID,用于设定 T1-T5 概率
|
||||||
|
|||||||
@@ -107,31 +107,6 @@ class DiceRewardConfigRecordController extends BaseController
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 更新数据
|
|
||||||
* @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);
|
|
||||||
$model = $this->logic->read($data['id'] ?? 0);
|
|
||||||
if ($model) {
|
|
||||||
$recordDeptId = is_array($model) ? ($model['dept_id'] ?? null) : ($model->dept_id ?? null);
|
|
||||||
if (! AdminScopeHelper::canAccessDept($this->adminInfo ?? null, $recordDeptId, $request->input('dept_id'))) {
|
|
||||||
return $this->fail('no permission to update this record');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
$result = $this->logic->edit($data['id'], $data);
|
|
||||||
if ($result) {
|
|
||||||
return $this->success('update success');
|
|
||||||
} else {
|
|
||||||
return $this->fail('update failed');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 删除数据
|
* 删除数据
|
||||||
* @param Request $request
|
* @param Request $request
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ class DiceAnteConfigLogic extends DiceBaseLogic
|
|||||||
public function add(array $data): mixed
|
public function add(array $data): mixed
|
||||||
{
|
{
|
||||||
return $this->transaction(function () use ($data) {
|
return $this->transaction(function () use ($data) {
|
||||||
|
$this->applyNameTitleFromMult($data);
|
||||||
$this->normalizeDefaultField($data);
|
$this->normalizeDefaultField($data);
|
||||||
$deptId = AdminScopeHelper::resolveConfigDeptId(null, $data['dept_id'] ?? AdminScopeHelper::DEFAULT_TEMPLATE_DEPT);
|
$deptId = AdminScopeHelper::resolveConfigDeptId(null, $data['dept_id'] ?? AdminScopeHelper::DEFAULT_TEMPLATE_DEPT);
|
||||||
if ((int) ($data['is_default'] ?? 0) === 1) {
|
if ((int) ($data['is_default'] ?? 0) === 1) {
|
||||||
@@ -38,6 +39,7 @@ class DiceAnteConfigLogic extends DiceBaseLogic
|
|||||||
$pickedDeptId = AdminScopeHelper::pickRequestDeptId($requestDeptId, $data);
|
$pickedDeptId = AdminScopeHelper::pickRequestDeptId($requestDeptId, $data);
|
||||||
$deptId = AdminScopeHelper::resolveConfigDeptId($adminInfo, $pickedDeptId);
|
$deptId = AdminScopeHelper::resolveConfigDeptId($adminInfo, $pickedDeptId);
|
||||||
return $this->transaction(function () use ($id, $data, $deptId, $adminInfo, $pickedDeptId) {
|
return $this->transaction(function () use ($id, $data, $deptId, $adminInfo, $pickedDeptId) {
|
||||||
|
$this->applyNameTitleFromMult($data);
|
||||||
$this->normalizeDefaultField($data);
|
$this->normalizeDefaultField($data);
|
||||||
if ((int) ($data['is_default'] ?? 0) === 1) {
|
if ((int) ($data['is_default'] ?? 0) === 1) {
|
||||||
$this->clearOtherDefaults((int) $id, $deptId);
|
$this->clearOtherDefaults((int) $id, $deptId);
|
||||||
@@ -92,6 +94,21 @@ class DiceAnteConfigLogic extends DiceBaseLogic
|
|||||||
$data['is_default'] = ((int) $data['is_default']) === 1 ? 1 : 0;
|
$data['is_default'] = ((int) $data['is_default']) === 1 ? 1 : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 名称、标题随底注倍率自动设为 xN */
|
||||||
|
private function applyNameTitleFromMult(array &$data): void
|
||||||
|
{
|
||||||
|
if (!array_key_exists('mult', $data)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$mult = (int) $data['mult'];
|
||||||
|
if ($mult <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$label = 'x' . $mult;
|
||||||
|
$data['name'] = $label;
|
||||||
|
$data['title'] = $label;
|
||||||
|
}
|
||||||
|
|
||||||
private function clearOtherDefaults(?int $excludeId = null, int $deptId = AdminScopeHelper::DEFAULT_TEMPLATE_DEPT): void
|
private function clearOtherDefaults(?int $excludeId = null, int $deptId = AdminScopeHelper::DEFAULT_TEMPLATE_DEPT): void
|
||||||
{
|
{
|
||||||
$query = $this->model->where('is_default', 1);
|
$query = $this->model->where('is_default', 1);
|
||||||
|
|||||||
@@ -6,9 +6,9 @@
|
|||||||
// +----------------------------------------------------------------------
|
// +----------------------------------------------------------------------
|
||||||
namespace app\dice\logic\config;
|
namespace app\dice\logic\config;
|
||||||
|
|
||||||
|
use app\dice\basic\DiceBaseLogic;
|
||||||
use app\dice\helper\AdminScopeHelper;
|
use app\dice\helper\AdminScopeHelper;
|
||||||
use app\dice\helper\ConfigScopeEditHelper;
|
use app\dice\helper\ConfigScopeEditHelper;
|
||||||
use plugin\saiadmin\basic\eloquent\BaseLogic;
|
|
||||||
use plugin\saiadmin\exception\ApiException;
|
use plugin\saiadmin\exception\ApiException;
|
||||||
use plugin\saiadmin\utils\Helper;
|
use plugin\saiadmin\utils\Helper;
|
||||||
use app\dice\model\config\DiceConfig;
|
use app\dice\model\config\DiceConfig;
|
||||||
|
|||||||
@@ -4,9 +4,9 @@
|
|||||||
// +----------------------------------------------------------------------
|
// +----------------------------------------------------------------------
|
||||||
namespace app\dice\logic\game;
|
namespace app\dice\logic\game;
|
||||||
|
|
||||||
|
use app\dice\basic\DiceBaseLogic;
|
||||||
use app\dice\helper\AdminScopeHelper;
|
use app\dice\helper\AdminScopeHelper;
|
||||||
use app\dice\helper\ConfigScopeEditHelper;
|
use app\dice\helper\ConfigScopeEditHelper;
|
||||||
use plugin\saiadmin\basic\eloquent\BaseLogic;
|
|
||||||
use app\dice\model\game\DiceGame;
|
use app\dice\model\game\DiceGame;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -6,13 +6,17 @@
|
|||||||
// +----------------------------------------------------------------------
|
// +----------------------------------------------------------------------
|
||||||
namespace app\dice\logic\lottery_pool_config;
|
namespace app\dice\logic\lottery_pool_config;
|
||||||
|
|
||||||
|
use app\api\cache\UserCache;
|
||||||
|
use app\api\service\LotteryService;
|
||||||
use app\dice\helper\AdminScopeHelper;
|
use app\dice\helper\AdminScopeHelper;
|
||||||
use app\dice\helper\ConfigScopeEditHelper;
|
use app\dice\helper\ConfigScopeEditHelper;
|
||||||
use app\dice\model\lottery_pool_config\DiceLotteryPoolConfig;
|
use app\dice\model\lottery_pool_config\DiceLotteryPoolConfig;
|
||||||
|
use app\dice\model\player\DicePlayer;
|
||||||
use app\dice\basic\DiceBaseLogic;
|
use app\dice\basic\DiceBaseLogic;
|
||||||
use plugin\saiadmin\exception\ApiException;
|
use plugin\saiadmin\exception\ApiException;
|
||||||
use plugin\saiadmin\utils\Helper;
|
use plugin\saiadmin\utils\Helper;
|
||||||
use support\think\Cache;
|
use support\think\Cache;
|
||||||
|
use support\think\Db;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 色子奖池配置逻辑层
|
* 色子奖池配置逻辑层
|
||||||
@@ -39,7 +43,7 @@ class DiceLotteryPoolConfigLogic extends DiceBaseLogic
|
|||||||
{
|
{
|
||||||
$pickedDeptId = AdminScopeHelper::pickRequestDeptId($requestDeptId, $data);
|
$pickedDeptId = AdminScopeHelper::pickRequestDeptId($requestDeptId, $data);
|
||||||
$deptId = AdminScopeHelper::resolveConfigDeptId($adminInfo, $pickedDeptId);
|
$deptId = AdminScopeHelper::resolveConfigDeptId($adminInfo, $pickedDeptId);
|
||||||
return ConfigScopeEditHelper::updateByPkAndDept(
|
$result = ConfigScopeEditHelper::updateByPkAndDept(
|
||||||
$this->model,
|
$this->model,
|
||||||
$id,
|
$id,
|
||||||
$deptId,
|
$deptId,
|
||||||
@@ -48,6 +52,106 @@ class DiceLotteryPoolConfigLogic extends DiceBaseLogic
|
|||||||
$adminInfo,
|
$adminInfo,
|
||||||
$pickedDeptId
|
$pickedDeptId
|
||||||
);
|
);
|
||||||
|
if ($result) {
|
||||||
|
$pool = DiceLotteryPoolConfig::where('id', $id)->find();
|
||||||
|
if ($pool && $pool->isPlayerDefaultTemplate()) {
|
||||||
|
$this->syncPlayersBoundToPlayerDefaultPool($pool);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 修改 playerDefault 后:同步同渠道所有绑定该池(及应跟随 playerDefault 的玩家)T1–T5 权重,并刷新 Redis
|
||||||
|
*
|
||||||
|
* @return int 已同步玩家数量
|
||||||
|
*/
|
||||||
|
public function syncPlayersBoundToPlayerDefaultPool(DiceLotteryPoolConfig $pool): int
|
||||||
|
{
|
||||||
|
$poolId = (int) ($pool->id ?? 0);
|
||||||
|
if ($poolId <= 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
$fresh = DiceLotteryPoolConfig::where('id', $poolId)->find();
|
||||||
|
if (!$fresh || !$fresh->isPlayerDefaultTemplate()) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
$pool = $fresh;
|
||||||
|
|
||||||
|
$weights = $this->extractPoolTierWeights($pool);
|
||||||
|
$poolDeptId = AdminScopeHelper::normalizeRecordDeptId($pool->dept_id ?? null);
|
||||||
|
$legacyDefaultPoolId = $this->findDefaultPoolIdForDept($poolDeptId);
|
||||||
|
|
||||||
|
$idsMap = [];
|
||||||
|
|
||||||
|
$boundPlayers = DicePlayer::where('lottery_config_id', $poolId)->select();
|
||||||
|
foreach ($boundPlayers as $player) {
|
||||||
|
$idsMap[(int) $player->id] = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($legacyDefaultPoolId > 0 && $legacyDefaultPoolId !== $poolId) {
|
||||||
|
$legacyPlayers = DicePlayer::where('lottery_config_id', $legacyDefaultPoolId)->select();
|
||||||
|
foreach ($legacyPlayers as $player) {
|
||||||
|
if (AdminScopeHelper::resolvePlayerConfigDeptId($player) === $poolDeptId) {
|
||||||
|
$idsMap[(int) $player->id] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$unboundPlayers = DicePlayer::where(function ($q) {
|
||||||
|
$q->where('lottery_config_id', 0)->whereOr('lottery_config_id', null);
|
||||||
|
})->select();
|
||||||
|
foreach ($unboundPlayers as $player) {
|
||||||
|
if (AdminScopeHelper::resolvePlayerConfigDeptId($player) === $poolDeptId) {
|
||||||
|
$idsMap[(int) $player->id] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($idsMap === []) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
$ids = array_keys($idsMap);
|
||||||
|
$update = array_merge($weights, ['lottery_config_id' => $poolId]);
|
||||||
|
Db::table('dice_player')->whereIn('id', $ids)->update($update);
|
||||||
|
|
||||||
|
foreach ($ids as $playerId) {
|
||||||
|
LotteryService::patchPlayerWeightsCache($playerId, $weights);
|
||||||
|
LotteryService::invalidatePlayerLotteryCache($playerId);
|
||||||
|
UserCache::deleteUser($playerId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return count($ids);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array{t1_weight:float,t2_weight:float,t3_weight:float,t4_weight:float,t5_weight:float}
|
||||||
|
*/
|
||||||
|
private function extractPoolTierWeights(DiceLotteryPoolConfig $pool): array
|
||||||
|
{
|
||||||
|
$data = $pool->getData();
|
||||||
|
return [
|
||||||
|
't1_weight' => (float) ($data['t1_weight'] ?? 0),
|
||||||
|
't2_weight' => (float) ($data['t2_weight'] ?? 0),
|
||||||
|
't3_weight' => (float) ($data['t3_weight'] ?? 0),
|
||||||
|
't4_weight' => (float) ($data['t4_weight'] ?? 0),
|
||||||
|
't5_weight' => (float) ($data['t5_weight'] ?? 0),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function findDefaultPoolIdForDept(int $deptId): int
|
||||||
|
{
|
||||||
|
$query = DiceLotteryPoolConfig::where('name', 'default');
|
||||||
|
if (AdminScopeHelper::isTemplateDeptId($deptId)) {
|
||||||
|
$query->where(function ($q) {
|
||||||
|
$q->where('dept_id', AdminScopeHelper::DEFAULT_TEMPLATE_DEPT)
|
||||||
|
->whereOr('dept_id', null);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
$query->where('dept_id', $deptId);
|
||||||
|
}
|
||||||
|
$row = $query->find();
|
||||||
|
return $row ? (int) $row->id : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
namespace app\dice\logic\player;
|
namespace app\dice\logic\player;
|
||||||
|
|
||||||
use app\dice\basic\DiceBaseLogic;
|
use app\dice\basic\DiceBaseLogic;
|
||||||
|
use app\dice\model\lottery_pool_config\DiceLotteryPoolConfig;
|
||||||
use plugin\saiadmin\exception\ApiException;
|
use plugin\saiadmin\exception\ApiException;
|
||||||
use plugin\saiadmin\utils\Helper;
|
use plugin\saiadmin\utils\Helper;
|
||||||
use app\dice\model\player\DicePlayer;
|
use app\dice\model\player\DicePlayer;
|
||||||
@@ -48,9 +49,30 @@ class DicePlayerLogic extends DiceBaseLogic
|
|||||||
} else {
|
} else {
|
||||||
unset($data['password']);
|
unset($data['password']);
|
||||||
}
|
}
|
||||||
|
$data = $this->applyLotteryPoolWeightsToPlayerData($data);
|
||||||
return parent::edit($id, $data);
|
return parent::edit($id, $data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 已绑定彩金池时:玩家 T1–T5 以池配置为准,避免前端提交陈旧权重覆盖同步结果
|
||||||
|
*/
|
||||||
|
private function applyLotteryPoolWeightsToPlayerData(array $data): array
|
||||||
|
{
|
||||||
|
$configId = isset($data['lottery_config_id']) ? (int) $data['lottery_config_id'] : 0;
|
||||||
|
if ($configId <= 0) {
|
||||||
|
return $data;
|
||||||
|
}
|
||||||
|
$config = DiceLotteryPoolConfig::find($configId);
|
||||||
|
if (!$config) {
|
||||||
|
return $data;
|
||||||
|
}
|
||||||
|
$row = $config->getData();
|
||||||
|
foreach (['t1_weight', 't2_weight', 't3_weight', 't4_weight', 't5_weight'] as $field) {
|
||||||
|
$data[$field] = $row[$field] ?? 0;
|
||||||
|
}
|
||||||
|
return $data;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 密码加密:md5(salt . password)
|
* 密码加密:md5(salt . password)
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -6,7 +6,6 @@
|
|||||||
// +----------------------------------------------------------------------
|
// +----------------------------------------------------------------------
|
||||||
namespace app\dice\logic\player_wallet_record;
|
namespace app\dice\logic\player_wallet_record;
|
||||||
|
|
||||||
use app\dice\helper\AdminScopeHelper;
|
|
||||||
use app\dice\basic\DiceBaseLogic;
|
use app\dice\basic\DiceBaseLogic;
|
||||||
use plugin\saiadmin\exception\ApiException;
|
use plugin\saiadmin\exception\ApiException;
|
||||||
use app\dice\model\player_wallet_record\DicePlayerWalletRecord;
|
use app\dice\model\player_wallet_record\DicePlayerWalletRecord;
|
||||||
@@ -28,18 +27,6 @@ class DicePlayerWalletRecordLogic extends DiceBaseLogic
|
|||||||
$this->setOrderType('DESC');
|
$this->setOrderType('DESC');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 按与列表相同的筛选条件汇总平台币变化(不含 with / 分页 / 排序)
|
|
||||||
*/
|
|
||||||
public function sumCoinBySearch(array $where, ?array $adminInfo, $requestDeptId = null): float
|
|
||||||
{
|
|
||||||
$query = $this->search($where);
|
|
||||||
AdminScopeHelper::applyAdminScope($query, $adminInfo, $requestDeptId);
|
|
||||||
$table = $this->model->getTable();
|
|
||||||
$sum = $query->sum($table . '.coin');
|
|
||||||
return round((float) $sum, 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 添加数据(补全抽奖次数字段默认值)
|
* 添加数据(补全抽奖次数字段默认值)
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -20,6 +20,16 @@ class DiceRewardLogic
|
|||||||
private const WEIGHT_MIN = 1;
|
private const WEIGHT_MIN = 1;
|
||||||
private const WEIGHT_MAX = 10000;
|
private const WEIGHT_MAX = 10000;
|
||||||
|
|
||||||
|
/** 豹子边点 5/30:对照表默认最低权重 */
|
||||||
|
private const CORNER_BIGWIN_GRIDS = [5, 30];
|
||||||
|
|
||||||
|
/** 中间大奖候选点 10/15/20/25:对照表默认权重 */
|
||||||
|
private const MID_BIGWIN_GRIDS = [10, 15, 20, 25];
|
||||||
|
|
||||||
|
private const REFERENCE_MID_WEIGHT = 10;
|
||||||
|
|
||||||
|
private const REFERENCE_NORMAL_WEIGHT = 100;
|
||||||
|
|
||||||
/** 档位键 */
|
/** 档位键 */
|
||||||
private const TIER_KEYS = ['T1', 'T2', 'T3', 'T4', 'T5', 'BIGWIN'];
|
private const TIER_KEYS = ['T1', 'T2', 'T3', 'T4', 'T5', 'BIGWIN'];
|
||||||
|
|
||||||
@@ -43,19 +53,21 @@ class DiceRewardLogic
|
|||||||
$orderField = isset($where['orderField']) && $where['orderField'] !== '' ? (string) $where['orderField'] : 'r.tier';
|
$orderField = isset($where['orderField']) && $where['orderField'] !== '' ? (string) $where['orderField'] : 'r.tier';
|
||||||
$orderType = isset($where['orderType']) && strtoupper((string) $where['orderType']) === 'DESC' ? 'desc' : 'asc';
|
$orderType = isset($where['orderType']) && strtoupper((string) $where['orderType']) === 'DESC' ? 'desc' : 'asc';
|
||||||
|
|
||||||
|
$keepIds = $this->resolveDedupedRewardIdsByGrid($direction, $tier, $adminInfo, $requestDeptId);
|
||||||
|
if ($keepIds === []) {
|
||||||
|
return [
|
||||||
|
'total' => 0,
|
||||||
|
'per_page' => $limit,
|
||||||
|
'current_page' => $page,
|
||||||
|
'data' => [],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
$query = DiceReward::alias('r')
|
$query = DiceReward::alias('r')
|
||||||
->where('r.direction', $direction)
|
->whereIn('r.id', $keepIds)
|
||||||
->field('r.id,r.tier,r.direction,r.end_index,r.weight,r.grid_number,r.start_index,r.ui_text,r.real_ev,r.remark,r.type,r.create_time,r.update_time')
|
->field('r.id,r.tier,r.direction,r.end_index,r.weight,r.grid_number,r.start_index,r.ui_text,r.real_ev,r.remark,r.type,r.create_time,r.update_time')
|
||||||
->order($orderField, $orderType)
|
->order($orderField, $orderType)
|
||||||
->order('r.end_index', 'asc');
|
->order('r.grid_number', 'asc');
|
||||||
|
|
||||||
if ($adminInfo !== null) {
|
|
||||||
AdminScopeHelper::applyConfigScope($query, $adminInfo, $requestDeptId, 'r.dept_id');
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($tier !== '') {
|
|
||||||
$query->where('r.tier', $tier);
|
|
||||||
}
|
|
||||||
|
|
||||||
$paginator = $query->paginate($limit, false, ['page' => $page]);
|
$paginator = $query->paginate($limit, false, ['page' => $page]);
|
||||||
$arr = $paginator->toArray();
|
$arr = $paginator->toArray();
|
||||||
@@ -81,6 +93,41 @@ class DiceRewardLogic
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 列表去重:每个方向、每个色子点数(5-30)仅保留一条(取 id 最大),避免历史重复数据导致 104 条
|
||||||
|
* @return int[]
|
||||||
|
*/
|
||||||
|
private function resolveDedupedRewardIdsByGrid(
|
||||||
|
int $direction,
|
||||||
|
string $tier,
|
||||||
|
?array $adminInfo,
|
||||||
|
$requestDeptId
|
||||||
|
): array {
|
||||||
|
$dedupeQuery = DiceReward::alias('rd')
|
||||||
|
->field('MAX(rd.id) AS keep_id')
|
||||||
|
->where('rd.direction', $direction)
|
||||||
|
->whereBetween('rd.grid_number', [5, 30]);
|
||||||
|
|
||||||
|
if ($adminInfo !== null) {
|
||||||
|
AdminScopeHelper::applyConfigScope($dedupeQuery, $adminInfo, $requestDeptId, 'rd.dept_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($tier !== '') {
|
||||||
|
$dedupeQuery->where('rd.tier', $tier);
|
||||||
|
}
|
||||||
|
|
||||||
|
$rows = $dedupeQuery->group('rd.grid_number')->select()->toArray();
|
||||||
|
$ids = [];
|
||||||
|
foreach ($rows as $row) {
|
||||||
|
$id = isset($row['keep_id']) ? (int) $row['keep_id'] : 0;
|
||||||
|
if ($id > 0) {
|
||||||
|
$ids[] = $id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $ids;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 按单方向批量更新权重(仅更新当前方向的 weight,并刷新缓存)
|
* 按单方向批量更新权重(仅更新当前方向的 weight,并刷新缓存)
|
||||||
* @param int $direction 0=顺时针 1=逆时针
|
* @param int $direction 0=顺时针 1=逆时针
|
||||||
@@ -327,6 +374,94 @@ class DiceRewardLogic
|
|||||||
private const GRID_NUMBER_MIN = 5;
|
private const GRID_NUMBER_MIN = 5;
|
||||||
private const GRID_NUMBER_MAX = 30;
|
private const GRID_NUMBER_MAX = 30;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 预览:按当前 dice_reward_config 计算将要生成的 dice_reward(不写库)
|
||||||
|
* 若当前 dice_reward 与计算结果完全一致,则标记 unchanged=true,并返回现有权重(导入时将复用旧权重)
|
||||||
|
*
|
||||||
|
* @return array{unchanged: bool, skipped: int, preview: array<string, array{0: array, 1: array}>}
|
||||||
|
* @throws ApiException
|
||||||
|
*/
|
||||||
|
public function createRewardReferencePreviewFromConfig(?int $deptId = null): array
|
||||||
|
{
|
||||||
|
$normalizedDeptId = $deptId;
|
||||||
|
$list = $this->loadConfigListForReference($normalizedDeptId);
|
||||||
|
$computed = $this->computeReferenceRowsFromConfigList($list, $normalizedDeptId);
|
||||||
|
|
||||||
|
$existing = $this->loadExistingRewardRowsForReference($normalizedDeptId);
|
||||||
|
$compare = $this->compareReferenceRows($computed['rows'], $existing);
|
||||||
|
$unchanged = $compare['unchanged'];
|
||||||
|
|
||||||
|
$previewRows = [];
|
||||||
|
foreach ($computed['rows'] as $row) {
|
||||||
|
$key = $row['direction'] . ':' . $row['grid_number'];
|
||||||
|
$gridNumber = isset($row['grid_number']) ? (int) $row['grid_number'] : 0;
|
||||||
|
$weight = $unchanged
|
||||||
|
? self::WEIGHT_MIN
|
||||||
|
: $this->defaultReferenceWeightForGrid($gridNumber);
|
||||||
|
$oldStart = null;
|
||||||
|
$oldEnd = null;
|
||||||
|
$oldTier = null;
|
||||||
|
$oldRemark = null;
|
||||||
|
$oldWeight = null;
|
||||||
|
if (isset($existing[$key])) {
|
||||||
|
$oldStart = isset($existing[$key]['start_index']) ? (int) $existing[$key]['start_index'] : null;
|
||||||
|
$oldEnd = isset($existing[$key]['end_index']) ? (int) $existing[$key]['end_index'] : null;
|
||||||
|
$oldTier = isset($existing[$key]['tier']) ? (string) $existing[$key]['tier'] : null;
|
||||||
|
$oldRemark = isset($existing[$key]['remark']) ? (string) $existing[$key]['remark'] : null;
|
||||||
|
$oldWeight = isset($existing[$key]['weight']) ? (int) $existing[$key]['weight'] : null;
|
||||||
|
}
|
||||||
|
// 映射未变化时:通常复用旧权重;旧权重为 1 时按点数类型补全为新默认建议值
|
||||||
|
if ($unchanged && $oldWeight !== null) {
|
||||||
|
$oldWeight = (int) $oldWeight;
|
||||||
|
$weight = max(self::WEIGHT_MIN, min(self::WEIGHT_MAX, $oldWeight));
|
||||||
|
if ($weight === self::WEIGHT_MIN) {
|
||||||
|
$weight = $this->defaultReferenceWeightForGrid($gridNumber);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$diffChanged = false;
|
||||||
|
$diffFields = [];
|
||||||
|
if ($oldStart === null || $oldEnd === null || $oldTier === null) {
|
||||||
|
$diffChanged = true;
|
||||||
|
$diffFields[] = 'new';
|
||||||
|
} else {
|
||||||
|
if ((int) $oldStart !== (int) ($row['start_index'] ?? 0)) {
|
||||||
|
$diffChanged = true;
|
||||||
|
$diffFields[] = 'start_index';
|
||||||
|
}
|
||||||
|
if ((int) $oldEnd !== (int) ($row['end_index'] ?? 0)) {
|
||||||
|
$diffChanged = true;
|
||||||
|
$diffFields[] = 'end_index';
|
||||||
|
}
|
||||||
|
if (trim((string) $oldTier) !== trim((string) ($row['tier'] ?? ''))) {
|
||||||
|
$diffChanged = true;
|
||||||
|
$diffFields[] = 'tier';
|
||||||
|
}
|
||||||
|
if (trim((string) $oldRemark) !== trim((string) ($row['remark'] ?? ''))) {
|
||||||
|
$diffChanged = true;
|
||||||
|
$diffFields[] = 'remark';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$previewRows[] = array_merge($row, [
|
||||||
|
'weight' => $weight,
|
||||||
|
'old_start_index' => $oldStart,
|
||||||
|
'old_end_index' => $oldEnd,
|
||||||
|
'old_tier' => $oldTier,
|
||||||
|
'old_remark' => $oldRemark,
|
||||||
|
'old_weight' => $oldWeight,
|
||||||
|
'diff_changed' => $diffChanged,
|
||||||
|
'diff_fields' => $diffFields,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'unchanged' => $unchanged,
|
||||||
|
'skipped' => $computed['skipped'],
|
||||||
|
'preview' => $this->groupReferenceRowsByTierWithDirection($previewRows),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 创建奖励对照:先清空 dice_reward 表,再按两种方向为点数 5-30 生成记录。
|
* 创建奖励对照:先清空 dice_reward 表,再按两种方向为点数 5-30 生成记录。
|
||||||
*
|
*
|
||||||
@@ -339,9 +474,9 @@ class DiceRewardLogic
|
|||||||
* - 奖励档位:tier = DiceRewardConfig::where('id', $end_index)->first()->tier
|
* - 奖励档位:tier = DiceRewardConfig::where('id', $end_index)->first()->tier
|
||||||
* - 显示ui:ui_text = DiceRewardConfig::where('id', $end_index)->first()->ui_text
|
* - 显示ui:ui_text = DiceRewardConfig::where('id', $end_index)->first()->ui_text
|
||||||
* - 实际中奖:real_ev = DiceRewardConfig::where('id', $end_index)->first()->real_ev
|
* - 实际中奖:real_ev = DiceRewardConfig::where('id', $end_index)->first()->real_ev
|
||||||
* - 备注:remark = DiceRewardConfig::where('id', $end_index)->first()->remark
|
* - 备注:remark = 按本条对照档位(推断后 T1-T5)的默认备注(T1 大奖、T2 小赚…),非落点格盘面 remark 字段
|
||||||
* - 类型:type = DiceRewardConfig::where('id', $end_index)->first()->type(-2=唯一惩罚,-1=抽水,0=回本,1=再来一次,2=小赚,3=大奖格)
|
* - 类型:type = DiceRewardConfig::where('id', $end_index)->first()->type(-2=唯一惩罚,-1=抽水,0=回本,1=再来一次,2=小赚,3=大奖格)
|
||||||
* - weight 默认 1,后续在权重编辑弹窗设置
|
* - weight 默认:10/15/20/25 为 10,5/30 为 1,其余为 100
|
||||||
*
|
*
|
||||||
* 例如顺时针摇取点数为 5 时:start_index = 配置中 grid_number=5 对应格位的 id,
|
* 例如顺时针摇取点数为 5 时:start_index = 配置中 grid_number=5 对应格位的 id,
|
||||||
* 结束位置 = (起始位置 + grid_number) % 26,再取该位置的 config 的 id 作为 end_index。
|
* 结束位置 = (起始位置 + grid_number) % 26,再取该位置的 config 的 id 作为 end_index。
|
||||||
@@ -352,10 +487,76 @@ class DiceRewardLogic
|
|||||||
* @throws ApiException
|
* @throws ApiException
|
||||||
*/
|
*/
|
||||||
public function createRewardReferenceFromConfig(?int $deptId = null): array
|
public function createRewardReferenceFromConfig(?int $deptId = null): array
|
||||||
|
{
|
||||||
|
$normalizedDeptId = $deptId;
|
||||||
|
$list = $this->loadConfigListForReference($normalizedDeptId);
|
||||||
|
$computed = $this->computeReferenceRowsFromConfigList($list, $normalizedDeptId);
|
||||||
|
|
||||||
|
$existing = $this->loadExistingRewardRowsForReference($normalizedDeptId);
|
||||||
|
$compare = $this->compareReferenceRows($computed['rows'], $existing);
|
||||||
|
if ($compare['unchanged']) {
|
||||||
|
return [
|
||||||
|
'created_clockwise' => 0,
|
||||||
|
'created_counterclockwise' => 0,
|
||||||
|
'updated_clockwise' => 0,
|
||||||
|
'updated_counterclockwise' => 0,
|
||||||
|
'skipped' => $computed['skipped'],
|
||||||
|
'unchanged' => true,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$table = (new DiceReward())->getTable();
|
||||||
|
if ($normalizedDeptId === null) {
|
||||||
|
Db::table($table)->whereNull('dept_id')->delete();
|
||||||
|
} else {
|
||||||
|
Db::table($table)->where('dept_id', $normalizedDeptId)->delete();
|
||||||
|
}
|
||||||
|
|
||||||
|
$createdCw = 0;
|
||||||
|
$createdCcw = 0;
|
||||||
|
foreach ($computed['rows'] as $row) {
|
||||||
|
$m = new DiceReward();
|
||||||
|
$m->tier = $row['tier'];
|
||||||
|
$m->direction = (int) $row['direction'];
|
||||||
|
$m->end_index = (int) $row['end_index'];
|
||||||
|
$m->weight = $this->defaultReferenceWeightForGrid((int) $row['grid_number']);
|
||||||
|
$m->grid_number = (int) $row['grid_number'];
|
||||||
|
$m->start_index = (int) $row['start_index'];
|
||||||
|
$m->ui_text = (string) ($row['ui_text'] ?? '');
|
||||||
|
$m->real_ev = $row['real_ev'] ?? null;
|
||||||
|
$m->remark = (string) ($row['remark'] ?? '');
|
||||||
|
$m->type = isset($row['type']) ? (int) $row['type'] : 0;
|
||||||
|
if ($normalizedDeptId !== null) {
|
||||||
|
$m->dept_id = $normalizedDeptId;
|
||||||
|
}
|
||||||
|
$m->save();
|
||||||
|
if ((int) $row['direction'] === DiceReward::DIRECTION_CLOCKWISE) {
|
||||||
|
$createdCw++;
|
||||||
|
} else {
|
||||||
|
$createdCcw++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DiceReward::refreshCache($normalizedDeptId ?? AdminScopeHelper::DEFAULT_TEMPLATE_DEPT);
|
||||||
|
return [
|
||||||
|
'created_clockwise' => $createdCw,
|
||||||
|
'created_counterclockwise' => $createdCcw,
|
||||||
|
'updated_clockwise' => 0,
|
||||||
|
'updated_counterclockwise' => 0,
|
||||||
|
'skipped' => $computed['skipped'],
|
||||||
|
'unchanged' => false,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 读取奖励配置(按 id asc),并把模板 dept 转为 null
|
||||||
|
* @return array<int, array<string, mixed>>
|
||||||
|
*/
|
||||||
|
private function loadConfigListForReference(?int &$deptId): array
|
||||||
{
|
{
|
||||||
$configQuery = DiceRewardConfig::order('id', 'asc');
|
$configQuery = DiceRewardConfig::order('id', 'asc');
|
||||||
if ($deptId === null || $deptId === \app\dice\helper\AdminScopeHelper::DEFAULT_TEMPLATE_DEPT) {
|
if ($deptId === null || $deptId === AdminScopeHelper::DEFAULT_TEMPLATE_DEPT) {
|
||||||
$templateId = \app\dice\helper\AdminScopeHelper::DEFAULT_TEMPLATE_DEPT;
|
$templateId = AdminScopeHelper::DEFAULT_TEMPLATE_DEPT;
|
||||||
$configQuery->where(function ($q) use ($templateId) {
|
$configQuery->where(function ($q) use ($templateId) {
|
||||||
$q->where('dept_id', $templateId)->whereOr('dept_id', 'null');
|
$q->where('dept_id', $templateId)->whereOr('dept_id', 'null');
|
||||||
});
|
});
|
||||||
@@ -367,25 +568,25 @@ class DiceRewardLogic
|
|||||||
if (empty($list)) {
|
if (empty($list)) {
|
||||||
throw new ApiException('Reward config is empty, please maintain dice_reward_config first');
|
throw new ApiException('Reward config is empty, please maintain dice_reward_config first');
|
||||||
}
|
}
|
||||||
$configCount = count($list);
|
if (count($list) < self::BOARD_SIZE) {
|
||||||
if ($configCount < self::BOARD_SIZE) {
|
|
||||||
throw new ApiException(
|
throw new ApiException(
|
||||||
\app\api\util\ApiLang::translateParams(
|
\app\api\util\ApiLang::translateParams(
|
||||||
'奖励配置需覆盖 26 个格位(id 0-25 或 1-26),当前仅 %s 条,无法完整生成 5-30 共26个点数、顺时针与逆时针的奖励对照',
|
'奖励配置需覆盖 26 个格位(id 0-25 或 1-26),当前仅 %s 条,无法完整生成 5-30 共26个点数、顺时针与逆时针的奖励对照',
|
||||||
[$configCount]
|
[count($list)]
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
return $list;
|
||||||
$table = (new DiceReward())->getTable();
|
|
||||||
if ($deptId === null) {
|
|
||||||
Db::table($table)->whereNull('dept_id')->delete();
|
|
||||||
} else {
|
|
||||||
Db::table($table)->where('dept_id', $deptId)->delete();
|
|
||||||
}
|
}
|
||||||
DiceReward::refreshCache($deptId ?? AdminScopeHelper::DEFAULT_TEMPLATE_DEPT);
|
|
||||||
|
|
||||||
// 按 id 排序后,盘面位置 0..25 对应 $list[$pos],避免 config.id 非 0-25/1-26 时取模结果找不到
|
/**
|
||||||
|
* 计算 5-30 两个方向的对照行(不含权重)
|
||||||
|
* @param array<int, array<string, mixed>> $list
|
||||||
|
* @return array{rows: array<int, array<string, mixed>>, skipped: int}
|
||||||
|
*/
|
||||||
|
private function computeReferenceRowsFromConfigList(array $list, ?int $deptId): array
|
||||||
|
{
|
||||||
|
// 按 id 排序后,盘面位置 0..25 对应 $list[$pos]
|
||||||
$gridToPosition = [];
|
$gridToPosition = [];
|
||||||
foreach ($list as $pos => $row) {
|
foreach ($list as $pos => $row) {
|
||||||
$gn = isset($row['grid_number']) ? (int) $row['grid_number'] : 0;
|
$gn = isset($row['grid_number']) ? (int) $row['grid_number'] : 0;
|
||||||
@@ -394,12 +595,8 @@ class DiceRewardLogic
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$createdCw = 0;
|
$rows = [];
|
||||||
$createdCcw = 0;
|
|
||||||
$updatedCw = 0;
|
|
||||||
$updatedCcw = 0;
|
|
||||||
$skipped = 0;
|
$skipped = 0;
|
||||||
|
|
||||||
for ($gridNumber = self::GRID_NUMBER_MIN; $gridNumber <= self::GRID_NUMBER_MAX; $gridNumber++) {
|
for ($gridNumber = self::GRID_NUMBER_MIN; $gridNumber <= self::GRID_NUMBER_MAX; $gridNumber++) {
|
||||||
if (!isset($gridToPosition[$gridNumber])) {
|
if (!isset($gridToPosition[$gridNumber])) {
|
||||||
$skipped++;
|
$skipped++;
|
||||||
@@ -414,115 +611,206 @@ class DiceRewardLogic
|
|||||||
|
|
||||||
$configCw = $list[$endPosCw] ?? null;
|
$configCw = $list[$endPosCw] ?? null;
|
||||||
$configCcw = $list[$endPosCcw] ?? null;
|
$configCcw = $list[$endPosCcw] ?? null;
|
||||||
$endIdCw = $configCw !== null && isset($configCw['id']) ? (int) $configCw['id'] : 0;
|
|
||||||
$endIdCcw = $configCcw !== null && isset($configCcw['id']) ? (int) $configCcw['id'] : 0;
|
|
||||||
|
|
||||||
if ($configCw !== null) {
|
if ($configCw !== null) {
|
||||||
$tier = isset($configCw['tier']) ? trim((string) $configCw['tier']) : '';
|
$tier = isset($configCw['tier']) ? trim((string) $configCw['tier']) : '';
|
||||||
if ($tier !== '') {
|
if ($tier !== '') {
|
||||||
// 使用对应奖励配置的 weight 作为格子权重(若未配置则退回最小权重)
|
$rows[] = $this->buildReferenceRowFromLandingConfig(
|
||||||
$weightCw = isset($configCw['weight']) && $configCw['weight'] !== null
|
$tier,
|
||||||
? $configCw['weight']
|
$configCw,
|
||||||
: self::WEIGHT_MIN;
|
DiceReward::DIRECTION_CLOCKWISE,
|
||||||
$payloadCw = [
|
$gridNumber,
|
||||||
'tier' => $tier,
|
$startId
|
||||||
'weight' => $weightCw,
|
);
|
||||||
'grid_number' => $gridNumber,
|
|
||||||
'start_index' => $startId,
|
|
||||||
'end_index' => $endIdCw,
|
|
||||||
'ui_text' => $configCw['ui_text'] ?? '',
|
|
||||||
'real_ev' => $configCw['real_ev'] ?? null,
|
|
||||||
'remark' => $configCw['remark'] ?? '',
|
|
||||||
'type' => isset($configCw['type']) ? (int) $configCw['type'] : 0,
|
|
||||||
];
|
|
||||||
$existingQuery = DiceReward::where('direction', DiceReward::DIRECTION_CLOCKWISE)->where('grid_number', $gridNumber);
|
|
||||||
if ($deptId === null) {
|
|
||||||
$existingQuery->whereNull('dept_id');
|
|
||||||
} else {
|
|
||||||
$existingQuery->where('dept_id', $deptId);
|
|
||||||
}
|
|
||||||
$existing = $existingQuery->find();
|
|
||||||
if ($existing) {
|
|
||||||
DiceReward::where('id', $existing->id)->update($payloadCw);
|
|
||||||
$updatedCw++;
|
|
||||||
} else {
|
|
||||||
$m = new DiceReward();
|
|
||||||
$m->tier = $tier;
|
|
||||||
$m->direction = DiceReward::DIRECTION_CLOCKWISE;
|
|
||||||
$m->end_index = $endIdCw;
|
|
||||||
$m->weight = $weightCw;
|
|
||||||
$m->grid_number = $gridNumber;
|
|
||||||
$m->start_index = $startId;
|
|
||||||
$m->ui_text = $configCw['ui_text'] ?? '';
|
|
||||||
$m->real_ev = $configCw['real_ev'] ?? null;
|
|
||||||
$m->remark = $configCw['remark'] ?? '';
|
|
||||||
$m->type = isset($configCw['type']) ? (int) $configCw['type'] : 0;
|
|
||||||
if ($deptId !== null) {
|
|
||||||
$m->dept_id = $deptId;
|
|
||||||
}
|
|
||||||
$m->save();
|
|
||||||
$createdCw++;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if ($configCcw !== null) {
|
if ($configCcw !== null) {
|
||||||
$tier = isset($configCcw['tier']) ? trim((string) $configCcw['tier']) : '';
|
$tier = isset($configCcw['tier']) ? trim((string) $configCcw['tier']) : '';
|
||||||
if ($tier !== '') {
|
if ($tier !== '') {
|
||||||
// 使用对应奖励配置的 weight 作为格子权重(若未配置则退回最小权重)
|
$rows[] = $this->buildReferenceRowFromLandingConfig(
|
||||||
$weightCcw = isset($configCcw['weight']) && $configCcw['weight'] !== null
|
$tier,
|
||||||
? $configCcw['weight']
|
$configCcw,
|
||||||
: self::WEIGHT_MIN;
|
DiceReward::DIRECTION_COUNTERCLOCKWISE,
|
||||||
$payloadCcw = [
|
$gridNumber,
|
||||||
'tier' => $tier,
|
$startId
|
||||||
'weight' => $weightCcw,
|
);
|
||||||
'grid_number' => $gridNumber,
|
|
||||||
'start_index' => $startId,
|
|
||||||
'end_index' => $endIdCcw,
|
|
||||||
'ui_text' => $configCcw['ui_text'] ?? '',
|
|
||||||
'real_ev' => $configCcw['real_ev'] ?? null,
|
|
||||||
'remark' => $configCcw['remark'] ?? '',
|
|
||||||
'type' => isset($configCcw['type']) ? (int) $configCcw['type'] : 0,
|
|
||||||
];
|
|
||||||
$existingQuery = DiceReward::where('direction', DiceReward::DIRECTION_COUNTERCLOCKWISE)->where('grid_number', $gridNumber);
|
|
||||||
if ($deptId === null) {
|
|
||||||
$existingQuery->whereNull('dept_id');
|
|
||||||
} else {
|
|
||||||
$existingQuery->where('dept_id', $deptId);
|
|
||||||
}
|
|
||||||
$existing = $existingQuery->find();
|
|
||||||
if ($existing) {
|
|
||||||
DiceReward::where('id', $existing->id)->update($payloadCcw);
|
|
||||||
$updatedCcw++;
|
|
||||||
} else {
|
|
||||||
$m = new DiceReward();
|
|
||||||
$m->tier = $tier;
|
|
||||||
$m->direction = DiceReward::DIRECTION_COUNTERCLOCKWISE;
|
|
||||||
$m->end_index = $endIdCcw;
|
|
||||||
$m->weight = $weightCcw;
|
|
||||||
$m->grid_number = $gridNumber;
|
|
||||||
$m->start_index = $startId;
|
|
||||||
$m->ui_text = $configCcw['ui_text'] ?? '';
|
|
||||||
$m->real_ev = $configCcw['real_ev'] ?? null;
|
|
||||||
$m->remark = $configCcw['remark'] ?? '';
|
|
||||||
$m->type = isset($configCcw['type']) ? (int) $configCcw['type'] : 0;
|
|
||||||
if ($deptId !== null) {
|
|
||||||
$m->dept_id = $deptId;
|
|
||||||
}
|
|
||||||
$m->save();
|
|
||||||
$createdCcw++;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return ['rows' => $rows, 'skipped' => $skipped];
|
||||||
}
|
}
|
||||||
|
|
||||||
DiceReward::refreshCache($deptId ?? AdminScopeHelper::DEFAULT_TEMPLATE_DEPT);
|
/**
|
||||||
|
* 对照表落点行:档位按落点格 real_ev 推断;备注随**本条对照档位**(T1 大奖 / T2 小赚 …),
|
||||||
|
* 与奖励配置页「按结算金额匹配档位备注」一致,不拷贝落点格盘面备注(盘面可保留「大奖格」等细分文案)。
|
||||||
|
*
|
||||||
|
* @param array<string, mixed> $landingConfig
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
private function buildReferenceRowFromLandingConfig(
|
||||||
|
string $tier,
|
||||||
|
array $landingConfig,
|
||||||
|
int $direction,
|
||||||
|
int $gridNumber,
|
||||||
|
int $startId
|
||||||
|
): array {
|
||||||
|
$realEv = isset($landingConfig['real_ev']) ? (float) $landingConfig['real_ev'] : 0.0;
|
||||||
|
if ($tier !== 'BIGWIN') {
|
||||||
|
$inferred = $this->inferTierFromRealEv($realEv);
|
||||||
|
if ($inferred !== '') {
|
||||||
|
$tier = $inferred;
|
||||||
|
}
|
||||||
|
}
|
||||||
return [
|
return [
|
||||||
'created_clockwise' => $createdCw,
|
'tier' => $tier,
|
||||||
'created_counterclockwise' => $createdCcw,
|
'direction' => $direction,
|
||||||
'updated_clockwise' => $updatedCw,
|
'weight' => $this->defaultReferenceWeightForGrid($gridNumber),
|
||||||
'updated_counterclockwise' => $updatedCcw,
|
'grid_number' => $gridNumber,
|
||||||
'skipped' => $skipped,
|
'start_index' => $startId,
|
||||||
|
'end_index' => isset($landingConfig['id']) ? (int) $landingConfig['id'] : 0,
|
||||||
|
'ui_text' => $landingConfig['ui_text'] ?? '',
|
||||||
|
'real_ev' => $landingConfig['real_ev'] ?? null,
|
||||||
|
'remark' => $this->defaultRemarkForTier($tier),
|
||||||
|
'type' => isset($landingConfig['type']) ? (int) $landingConfig['type'] : 0,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按结算金额推断档位(与前端 generateIndexByRules 一致)
|
||||||
|
*/
|
||||||
|
private function inferTierFromRealEv(float $realEv): string
|
||||||
|
{
|
||||||
|
if ($realEv > 2) {
|
||||||
|
return 'T1';
|
||||||
|
}
|
||||||
|
if ($realEv > 1) {
|
||||||
|
return 'T2';
|
||||||
|
}
|
||||||
|
if ($realEv > 0) {
|
||||||
|
return 'T3';
|
||||||
|
}
|
||||||
|
if ($realEv < 0) {
|
||||||
|
return 'T4';
|
||||||
|
}
|
||||||
|
return 'T5';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建奖励对照表时按色子点数和的默认权重
|
||||||
|
*/
|
||||||
|
private function defaultReferenceWeightForGrid(int $gridNumber): int
|
||||||
|
{
|
||||||
|
if (in_array($gridNumber, self::MID_BIGWIN_GRIDS, true)) {
|
||||||
|
return self::REFERENCE_MID_WEIGHT;
|
||||||
|
}
|
||||||
|
if (in_array($gridNumber, self::CORNER_BIGWIN_GRIDS, true)) {
|
||||||
|
return self::WEIGHT_MIN;
|
||||||
|
}
|
||||||
|
return self::REFERENCE_NORMAL_WEIGHT;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 档位默认备注
|
||||||
|
*/
|
||||||
|
private function defaultRemarkForTier(string $tier): string
|
||||||
|
{
|
||||||
|
return match ($tier) {
|
||||||
|
'T1', 'BIGWIN' => '大奖',
|
||||||
|
'T2' => '小赚',
|
||||||
|
'T3' => '抽水',
|
||||||
|
'T4' => '惩罚',
|
||||||
|
'T5' => '再来一次',
|
||||||
|
default => '',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 读出当前 dice_reward(用于对比/复用权重)。key = "direction:grid_number"
|
||||||
|
* @return array<string, array<string, mixed>>
|
||||||
|
*/
|
||||||
|
private function loadExistingRewardRowsForReference(?int $deptId): array
|
||||||
|
{
|
||||||
|
$query = DiceReward::whereIn('grid_number', range(self::GRID_NUMBER_MIN, self::GRID_NUMBER_MAX))
|
||||||
|
->whereIn('direction', [DiceReward::DIRECTION_CLOCKWISE, DiceReward::DIRECTION_COUNTERCLOCKWISE]);
|
||||||
|
if ($deptId === null) {
|
||||||
|
$query->whereNull('dept_id');
|
||||||
|
} else {
|
||||||
|
$query->where('dept_id', $deptId);
|
||||||
|
}
|
||||||
|
$rows = $query->select()->toArray();
|
||||||
|
$map = [];
|
||||||
|
foreach ($rows as $r) {
|
||||||
|
$dir = isset($r['direction']) ? (int) $r['direction'] : 0;
|
||||||
|
$gn = isset($r['grid_number']) ? (int) $r['grid_number'] : 0;
|
||||||
|
$map[$dir . ':' . $gn] = $r;
|
||||||
|
}
|
||||||
|
return $map;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 对比 computed 与 existing 是否完全一致(忽略权重)
|
||||||
|
* @param array<int, array<string, mixed>> $computedRows
|
||||||
|
* @param array<string, array<string, mixed>> $existingMap
|
||||||
|
* @return array{unchanged: bool}
|
||||||
|
*/
|
||||||
|
private function compareReferenceRows(array $computedRows, array $existingMap): array
|
||||||
|
{
|
||||||
|
if (empty($computedRows)) {
|
||||||
|
return ['unchanged' => false];
|
||||||
|
}
|
||||||
|
foreach ($computedRows as $row) {
|
||||||
|
$key = $row['direction'] . ':' . $row['grid_number'];
|
||||||
|
if (!isset($existingMap[$key])) {
|
||||||
|
return ['unchanged' => false];
|
||||||
|
}
|
||||||
|
$ex = $existingMap[$key];
|
||||||
|
$same =
|
||||||
|
((int) ($ex['start_index'] ?? 0) === (int) ($row['start_index'] ?? 0)) &&
|
||||||
|
((int) ($ex['end_index'] ?? 0) === (int) ($row['end_index'] ?? 0)) &&
|
||||||
|
(trim((string) ($ex['tier'] ?? '')) === trim((string) ($row['tier'] ?? ''))) &&
|
||||||
|
(trim((string) ($ex['remark'] ?? '')) === trim((string) ($row['remark'] ?? '')));
|
||||||
|
if (!$same) {
|
||||||
|
return ['unchanged' => false];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ['unchanged' => true];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将行按 tier -> {0:[],1:[]} 组织,便于前端展示(与 weightRatioList 输出结构一致)
|
||||||
|
* @param array<int, array<string, mixed>> $rows
|
||||||
|
* @return array<string, array{0: array, 1: array}>
|
||||||
|
*/
|
||||||
|
private function groupReferenceRowsByTierWithDirection(array $rows): array
|
||||||
|
{
|
||||||
|
$result = [];
|
||||||
|
foreach (self::TIER_KEYS as $tier) {
|
||||||
|
$result[$tier] = [0 => [], 1 => []];
|
||||||
|
}
|
||||||
|
foreach ($rows as $r) {
|
||||||
|
$tier = isset($r['tier']) ? trim((string) $r['tier']) : '';
|
||||||
|
if ($tier === '' || !isset($result[$tier])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$dir = isset($r['direction']) ? (int) $r['direction'] : 0;
|
||||||
|
$dir = $dir === 1 ? 1 : 0;
|
||||||
|
$result[$tier][$dir][] = [
|
||||||
|
'reward_id' => 0,
|
||||||
|
'id' => isset($r['end_index']) ? (int) $r['end_index'] : 0,
|
||||||
|
'grid_number' => isset($r['grid_number']) ? (int) $r['grid_number'] : 0,
|
||||||
|
'ui_text' => (string) ($r['ui_text'] ?? ''),
|
||||||
|
'real_ev' => $r['real_ev'] ?? 0,
|
||||||
|
'remark' => (string) ($r['remark'] ?? ''),
|
||||||
|
'weight' => isset($r['weight']) ? max(self::WEIGHT_MIN, min(self::WEIGHT_MAX, (int) $r['weight'])) : self::WEIGHT_MIN,
|
||||||
|
'start_index' => isset($r['start_index']) ? (int) $r['start_index'] : 0,
|
||||||
|
'tier' => (string) ($r['tier'] ?? ''),
|
||||||
|
'old_start_index' => $r['old_start_index'] ?? null,
|
||||||
|
'old_end_index' => $r['old_end_index'] ?? null,
|
||||||
|
'old_tier' => $r['old_tier'] ?? null,
|
||||||
|
'old_weight' => $r['old_weight'] ?? null,
|
||||||
|
'diff_changed' => (bool) ($r['diff_changed'] ?? false),
|
||||||
|
'diff_fields' => $r['diff_fields'] ?? [],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ use app\dice\model\ante_config\DiceAnteConfig;
|
|||||||
use app\dice\model\lottery_pool_config\DiceLotteryPoolConfig;
|
use app\dice\model\lottery_pool_config\DiceLotteryPoolConfig;
|
||||||
use app\dice\model\reward\DiceReward;
|
use app\dice\model\reward\DiceReward;
|
||||||
use app\dice\model\reward\DiceRewardConfig;
|
use app\dice\model\reward\DiceRewardConfig;
|
||||||
|
use app\dice\model\play_record_test\DicePlayRecordTest;
|
||||||
use app\dice\model\reward_config_record\DiceRewardConfigRecord;
|
use app\dice\model\reward_config_record\DiceRewardConfigRecord;
|
||||||
use app\dice\basic\DiceBaseLogic;
|
use app\dice\basic\DiceBaseLogic;
|
||||||
use plugin\saiadmin\exception\ApiException;
|
use plugin\saiadmin\exception\ApiException;
|
||||||
@@ -77,6 +78,51 @@ class DiceRewardConfigRecordLogic extends DiceBaseLogic
|
|||||||
return $map;
|
return $map;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除权重测试记录,并级联删除 dice_play_record_test 中 reward_config_record_id 关联的明细
|
||||||
|
* @param mixed $ids
|
||||||
|
*/
|
||||||
|
public function destroy($ids): bool
|
||||||
|
{
|
||||||
|
return $this->transaction(function () use ($ids) {
|
||||||
|
$intIds = $this->normalizeDestroyIds($ids);
|
||||||
|
if ($intIds === []) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
$this->destroyRelatedPlayRecordTests($intIds);
|
||||||
|
|
||||||
|
return parent::destroy($intIds);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param mixed $ids
|
||||||
|
* @return list<int>
|
||||||
|
*/
|
||||||
|
private function normalizeDestroyIds($ids): array
|
||||||
|
{
|
||||||
|
$idList = is_array($ids) ? $ids : explode(',', (string) $ids);
|
||||||
|
$intIds = [];
|
||||||
|
foreach ($idList as $v) {
|
||||||
|
$iv = (int) $v;
|
||||||
|
if ($iv > 0) {
|
||||||
|
$intIds[] = $iv;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return array_values(array_unique($intIds));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param list<int> $recordIds dice_reward_config_record.id
|
||||||
|
*/
|
||||||
|
private function destroyRelatedPlayRecordTests(array $recordIds): void
|
||||||
|
{
|
||||||
|
DicePlayRecordTest::destroy(function ($query) use ($recordIds) {
|
||||||
|
$query->whereIn('reward_config_record_id', $recordIds);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 将测试记录导入:DiceReward(权重快照)、DiceRewardConfig(BIGWIN weight)、DiceLotteryPoolConfig(付费/免费 T1-T5)
|
* 将测试记录导入:DiceReward(权重快照)、DiceRewardConfig(BIGWIN weight)、DiceLotteryPoolConfig(付费/免费 T1-T5)
|
||||||
* @param int $recordId 测试记录 ID
|
* @param int $recordId 测试记录 ID
|
||||||
@@ -268,7 +314,8 @@ class DiceRewardConfigRecordLogic extends DiceBaseLogic
|
|||||||
is_array($params) ? $params : []
|
is_array($params) ? $params : []
|
||||||
);
|
);
|
||||||
$allowed = [100, 500, 1000, 5000];
|
$allowed = [100, 500, 1000, 5000];
|
||||||
$ante = $this->resolveWeightTestAnte($params, $deptId);
|
$anteRandom = !empty($params['ante_random']);
|
||||||
|
$ante = $anteRandom ? 0 : $this->resolveWeightTestAnte($params, $deptId);
|
||||||
|
|
||||||
$lotteryConfigId = isset($params['lottery_config_id']) ? (int) $params['lottery_config_id'] : 0;
|
$lotteryConfigId = isset($params['lottery_config_id']) ? (int) $params['lottery_config_id'] : 0;
|
||||||
$paidConfigId = isset($params['paid_lottery_config_id']) ? (int) $params['paid_lottery_config_id'] : 0;
|
$paidConfigId = isset($params['paid_lottery_config_id']) ? (int) $params['paid_lottery_config_id'] : 0;
|
||||||
@@ -283,7 +330,12 @@ class DiceRewardConfigRecordLogic extends DiceBaseLogic
|
|||||||
$paidN = isset($params['paid_n_count']) ? (int) $params['paid_n_count'] : 0;
|
$paidN = isset($params['paid_n_count']) ? (int) $params['paid_n_count'] : 0;
|
||||||
$chainFreeMode = !empty($params['chain_free_mode']);
|
$chainFreeMode = !empty($params['chain_free_mode']);
|
||||||
$killModeEnabled = !empty($params['kill_mode_enabled']);
|
$killModeEnabled = !empty($params['kill_mode_enabled']);
|
||||||
$testSafetyLine = isset($params['test_safety_line']) ? (int) $params['test_safety_line'] : 5000;
|
if (array_key_exists('test_safety_line', $params) && $params['test_safety_line'] !== null && $params['test_safety_line'] !== '') {
|
||||||
|
$testSafetyLine = (int) $params['test_safety_line'];
|
||||||
|
} else {
|
||||||
|
$defaultPool = DiceLotteryPoolConfig::findByNameForDept('default', $deptId);
|
||||||
|
$testSafetyLine = (int) ($defaultPool->safety_line ?? 0);
|
||||||
|
}
|
||||||
if ($testSafetyLine < 0) {
|
if ($testSafetyLine < 0) {
|
||||||
throw new ApiException('test_safety_line must be greater than or equal to 0');
|
throw new ApiException('test_safety_line must be greater than or equal to 0');
|
||||||
}
|
}
|
||||||
@@ -405,6 +457,9 @@ class DiceRewardConfigRecordLogic extends DiceBaseLogic
|
|||||||
if ($chainFreeMode) {
|
if ($chainFreeMode) {
|
||||||
$tierWeightsSnapshot['chain_free_mode'] = true;
|
$tierWeightsSnapshot['chain_free_mode'] = true;
|
||||||
}
|
}
|
||||||
|
if (!empty($params['ante_random'])) {
|
||||||
|
$tierWeightsSnapshot['ante_random'] = true;
|
||||||
|
}
|
||||||
|
|
||||||
$record = new DiceRewardConfigRecord();
|
$record = new DiceRewardConfigRecord();
|
||||||
$plannedPaidSpins = $paidS + $paidN;
|
$plannedPaidSpins = $paidS + $paidN;
|
||||||
@@ -494,6 +549,21 @@ class DiceRewardConfigRecordLogic extends DiceBaseLogic
|
|||||||
*/
|
*/
|
||||||
private function resolveWeightTestAnte(array $params, int $deptId): int
|
private function resolveWeightTestAnte(array $params, int $deptId): int
|
||||||
{
|
{
|
||||||
|
if (!empty($params['ante_random'])) {
|
||||||
|
$anteQuery = DiceAnteConfig::field('id,mult')->order('mult', 'asc');
|
||||||
|
ConfigScopeEditHelper::applyDeptIdWhere($anteQuery, $deptId);
|
||||||
|
$rows = $anteQuery->select()->toArray();
|
||||||
|
if ($rows === []) {
|
||||||
|
throw new ApiException('No ante config in current channel');
|
||||||
|
}
|
||||||
|
$picked = $rows[random_int(0, count($rows) - 1)];
|
||||||
|
$mult = (int) ($picked['mult'] ?? 0);
|
||||||
|
if ($mult <= 0) {
|
||||||
|
throw new ApiException('ANTE_MUST_POSITIVE');
|
||||||
|
}
|
||||||
|
return $mult;
|
||||||
|
}
|
||||||
|
|
||||||
$anteConfigId = isset($params['ante_config_id']) ? (int) $params['ante_config_id'] : 0;
|
$anteConfigId = isset($params['ante_config_id']) ? (int) $params['ante_config_id'] : 0;
|
||||||
if ($anteConfigId > 0) {
|
if ($anteConfigId > 0) {
|
||||||
$config = DiceAnteConfig::find($anteConfigId);
|
$config = DiceAnteConfig::find($anteConfigId);
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ namespace app\dice\logic\reward_config_record;
|
|||||||
|
|
||||||
use app\api\logic\PlayStartLogic;
|
use app\api\logic\PlayStartLogic;
|
||||||
use app\dice\helper\AdminScopeHelper;
|
use app\dice\helper\AdminScopeHelper;
|
||||||
|
use app\dice\helper\ConfigScopeEditHelper;
|
||||||
|
use app\dice\model\ante_config\DiceAnteConfig;
|
||||||
use app\dice\model\lottery_pool_config\DiceLotteryPoolConfig;
|
use app\dice\model\lottery_pool_config\DiceLotteryPoolConfig;
|
||||||
use app\dice\model\play_record_test\DicePlayRecordTest;
|
use app\dice\model\play_record_test\DicePlayRecordTest;
|
||||||
use app\dice\model\reward_config_record\DiceRewardConfigRecord;
|
use app\dice\model\reward_config_record\DiceRewardConfigRecord;
|
||||||
@@ -15,7 +17,10 @@ use support\think\Db;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 一键测试权重:单进程后台执行模拟摇色子,写入 dice_play_record_test 并更新 dice_reward_config_record 进度
|
* 一键测试权重:单进程后台执行模拟摇色子,写入 dice_play_record_test 并更新 dice_reward_config_record 进度
|
||||||
* 支持测试内杀分:当模拟玩家累计盈利达到安全线后,付费抽奖切换到 killScore
|
* 抽奖规则与 PlayStartLogic 一致:
|
||||||
|
* - 付费未杀分:按模拟玩家档位权重抽档,lottery_config_id 记 default
|
||||||
|
* - 付费杀分:测试内杀分开启且模拟池盈利 >= test_safety_line 后切 killScore
|
||||||
|
* - 免费券:name=free 奖池(无则 default),排除 5/30 豹子
|
||||||
*/
|
*/
|
||||||
class WeightTestRunner
|
class WeightTestRunner
|
||||||
{
|
{
|
||||||
@@ -57,7 +62,6 @@ class WeightTestRunner
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$ante = is_numeric($record->ante ?? null) ? intval($record->ante) : 1;
|
|
||||||
$paidS = (int) ($record->paid_s_count ?? 0);
|
$paidS = (int) ($record->paid_s_count ?? 0);
|
||||||
$paidN = (int) ($record->paid_n_count ?? 0);
|
$paidN = (int) ($record->paid_n_count ?? 0);
|
||||||
$total = $paidS + $paidN;
|
$total = $paidS + $paidN;
|
||||||
@@ -68,11 +72,18 @@ class WeightTestRunner
|
|||||||
|
|
||||||
$this->runDeptId = $this->resolveRunDeptId($recordId, $record);
|
$this->runDeptId = $this->resolveRunDeptId($recordId, $record);
|
||||||
$deptId = $this->runDeptId;
|
$deptId = $this->runDeptId;
|
||||||
|
|
||||||
|
$anteRandom = $this->isAnteRandomMode($record);
|
||||||
|
$ante = is_numeric($record->ante ?? null) ? intval($record->ante) : 1;
|
||||||
|
if (!$anteRandom && $ante <= 0) {
|
||||||
|
$ante = $this->getMinAnteMult($deptId);
|
||||||
|
}
|
||||||
DiceReward::setRequestDeptId($deptId);
|
DiceReward::setRequestDeptId($deptId);
|
||||||
DiceRewardConfig::clearRequestInstance();
|
DiceRewardConfig::clearRequestInstance();
|
||||||
|
|
||||||
$configType0 = DiceLotteryPoolConfig::findByNameForDept('default', $deptId);
|
$configType0 = DiceLotteryPoolConfig::findByNameForDept('default', $deptId);
|
||||||
$configType1 = DiceLotteryPoolConfig::findByNameForDept('killScore', $deptId);
|
$configKill = DiceLotteryPoolConfig::findByNameForDept('killScore', $deptId);
|
||||||
|
$configFree = DiceLotteryPoolConfig::findByNameForDept('free', $deptId);
|
||||||
if (!$configType0) {
|
if (!$configType0) {
|
||||||
$this->markFailed($recordId, '彩金池配置 name=default 不存在(当前渠道)');
|
$this->markFailed($recordId, '彩金池配置 name=default 不存在(当前渠道)');
|
||||||
return;
|
return;
|
||||||
@@ -92,9 +103,9 @@ class WeightTestRunner
|
|||||||
if (!$paidPoolConfig || AdminScopeHelper::normalizeRecordDeptId($paidPoolConfig->dept_id ?? null) !== $deptId) {
|
if (!$paidPoolConfig || AdminScopeHelper::normalizeRecordDeptId($paidPoolConfig->dept_id ?? null) !== $deptId) {
|
||||||
$paidPoolConfig = $configType0;
|
$paidPoolConfig = $configType0;
|
||||||
}
|
}
|
||||||
$freePoolConfig = $freePoolConfigId > 0 ? DiceLotteryPoolConfig::find($freePoolConfigId) : $configType1;
|
$freePoolConfig = $freePoolConfigId > 0 ? DiceLotteryPoolConfig::find($freePoolConfigId) : $configFree;
|
||||||
if (!$freePoolConfig || AdminScopeHelper::normalizeRecordDeptId($freePoolConfig->dept_id ?? null) !== $deptId) {
|
if (!$freePoolConfig || AdminScopeHelper::normalizeRecordDeptId($freePoolConfig->dept_id ?? null) !== $deptId) {
|
||||||
$freePoolConfig = $configType1 ?: $configType0;
|
$freePoolConfig = $configFree ?: $configType0;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($paidTierWeightsCustom !== null && array_sum($paidTierWeightsCustom) <= 0) {
|
if ($paidTierWeightsCustom !== null && array_sum($paidTierWeightsCustom) <= 0) {
|
||||||
@@ -116,8 +127,11 @@ class WeightTestRunner
|
|||||||
$testSafetyLine = 0;
|
$testSafetyLine = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 测试内“玩家累计盈利”:用于控制付费局是否切换杀分
|
// 彩金池累计盈利:从 default.profit_amount 起步并在测试内逐局累加,与杀分判定值一致
|
||||||
$playerProfitTotal = 0.0;
|
$poolProfitTotal = (float) ($configType0->profit_amount ?? 0);
|
||||||
|
|
||||||
|
// 付费未杀分时的模拟玩家档位权重(自定义 > 快照 > 兜底奖池)
|
||||||
|
$paidPlayerWeights = $paidTierWeightsCustom ?? $this->resolveTierWeightsSnapshot($record, 'paid');
|
||||||
|
|
||||||
$playLogic = new PlayStartLogic();
|
$playLogic = new PlayStartLogic();
|
||||||
$resultCounts = [];
|
$resultCounts = [];
|
||||||
@@ -133,14 +147,16 @@ class WeightTestRunner
|
|||||||
$paidS,
|
$paidS,
|
||||||
$paidN,
|
$paidN,
|
||||||
$ante,
|
$ante,
|
||||||
|
$anteRandom,
|
||||||
|
$configType0,
|
||||||
$paidPoolConfig,
|
$paidPoolConfig,
|
||||||
$freePoolConfig,
|
$freePoolConfig,
|
||||||
$configType1,
|
$configKill,
|
||||||
$paidTierWeightsCustom,
|
$paidPlayerWeights,
|
||||||
$freeTierWeightsCustom,
|
$freeTierWeightsCustom,
|
||||||
$killModeEnabled,
|
$killModeEnabled,
|
||||||
$testSafetyLine,
|
$testSafetyLine,
|
||||||
$playerProfitTotal,
|
$poolProfitTotal,
|
||||||
$resultCounts,
|
$resultCounts,
|
||||||
$tierCounts,
|
$tierCounts,
|
||||||
$buffer,
|
$buffer,
|
||||||
@@ -172,14 +188,16 @@ class WeightTestRunner
|
|||||||
int $paidS,
|
int $paidS,
|
||||||
int $paidN,
|
int $paidN,
|
||||||
int $ante,
|
int $ante,
|
||||||
|
bool $anteRandom,
|
||||||
|
$defaultPoolConfig,
|
||||||
$paidPoolConfig,
|
$paidPoolConfig,
|
||||||
$freePoolConfig,
|
$freePoolConfig,
|
||||||
$killPoolConfig,
|
$killPoolConfig,
|
||||||
?array $paidTierWeightsCustom,
|
?array $paidPlayerWeights,
|
||||||
?array $freeTierWeightsCustom,
|
?array $freeTierWeightsCustom,
|
||||||
bool $killModeEnabled,
|
bool $killModeEnabled,
|
||||||
int $testSafetyLine,
|
int $testSafetyLine,
|
||||||
float &$playerProfitTotal,
|
float &$poolProfitTotal,
|
||||||
array &$resultCounts,
|
array &$resultCounts,
|
||||||
array &$tierCounts,
|
array &$tierCounts,
|
||||||
array &$buffer,
|
array &$buffer,
|
||||||
@@ -187,27 +205,46 @@ class WeightTestRunner
|
|||||||
): void {
|
): void {
|
||||||
$queue = [];
|
$queue = [];
|
||||||
for ($i = 0; $i < $paidS; $i++) {
|
for ($i = 0; $i < $paidS; $i++) {
|
||||||
$queue[] = ['paid', 0, $ante];
|
$queue[] = ['paid', 0, $anteRandom ? 0 : $ante];
|
||||||
}
|
}
|
||||||
for ($i = 0; $i < $paidN; $i++) {
|
for ($i = 0; $i < $paidN; $i++) {
|
||||||
$queue[] = ['paid', 1, $ante];
|
$queue[] = ['paid', 1, $anteRandom ? 0 : $ante];
|
||||||
}
|
}
|
||||||
$qi = 0;
|
$qi = 0;
|
||||||
|
$lastPaidPlayAnte = 0;
|
||||||
while ($qi < count($queue)) {
|
while ($qi < count($queue)) {
|
||||||
$item = $queue[$qi];
|
$item = $queue[$qi];
|
||||||
$isPaid = $item[0] === 'paid';
|
$isPaid = $item[0] === 'paid';
|
||||||
$dir = $item[1];
|
$dir = $item[1];
|
||||||
$playAnte = $item[2];
|
$playAnte = (int) $item[2];
|
||||||
|
$playAnte = $this->resolvePlayAnteMult(
|
||||||
|
$deptId,
|
||||||
|
$playAnte,
|
||||||
|
$isPaid,
|
||||||
|
$anteRandom,
|
||||||
|
$ante,
|
||||||
|
$lastPaidPlayAnte
|
||||||
|
);
|
||||||
|
if ($isPaid) {
|
||||||
|
$lastPaidPlayAnte = $playAnte;
|
||||||
|
}
|
||||||
$lotteryType = $isPaid ? 0 : 1;
|
$lotteryType = $isPaid ? 0 : 1;
|
||||||
|
|
||||||
if ($isPaid) {
|
if ($isPaid) {
|
||||||
$useKillForPaid = $killModeEnabled && $playerProfitTotal >= $testSafetyLine && $killPoolConfig !== null;
|
$useKillForPaid = $killModeEnabled
|
||||||
|
&& $poolProfitTotal >= $testSafetyLine
|
||||||
|
&& $killPoolConfig !== null;
|
||||||
if ($useKillForPaid) {
|
if ($useKillForPaid) {
|
||||||
$cfg = $killPoolConfig;
|
$cfg = $killPoolConfig;
|
||||||
$customWeights = null;
|
$customWeights = null;
|
||||||
} else {
|
} else {
|
||||||
|
// 付费未杀分:模拟玩家档位权重,lottery_config_id 记 default(与 PlayStartLogic 一致)
|
||||||
|
$cfg = $defaultPoolConfig;
|
||||||
|
$customWeights = $paidPlayerWeights;
|
||||||
|
if ($customWeights === null) {
|
||||||
$cfg = $paidPoolConfig;
|
$cfg = $paidPoolConfig;
|
||||||
$customWeights = $paidTierWeightsCustom;
|
$customWeights = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
$cfg = $freePoolConfig;
|
$cfg = $freePoolConfig;
|
||||||
@@ -215,9 +252,12 @@ class WeightTestRunner
|
|||||||
}
|
}
|
||||||
|
|
||||||
$row = $playLogic->simulateOnePlay($cfg, $dir, $lotteryType, $playAnte, $customWeights, $deptId);
|
$row = $playLogic->simulateOnePlay($cfg, $dir, $lotteryType, $playAnte, $customWeights, $deptId);
|
||||||
|
// 明细底注必须为 dice_ante_config.mult(随机模式每局独立抽取后的值)
|
||||||
|
$row['ante'] = $playAnte;
|
||||||
$winCoin = (float) ($row['win_coin'] ?? 0);
|
$winCoin = (float) ($row['win_coin'] ?? 0);
|
||||||
$paidAmount = (float) ($row['paid_amount'] ?? 0);
|
$paidAmount = (float) ($row['paid_amount'] ?? 0);
|
||||||
$playerProfitTotal += $winCoin - $paidAmount;
|
$perPlayProfit = $isPaid ? ($winCoin - $paidAmount) : $winCoin;
|
||||||
|
$poolProfitTotal += round($perPlayProfit, 2);
|
||||||
$this->aggregate($row, $resultCounts, $tierCounts);
|
$this->aggregate($row, $resultCounts, $tierCounts);
|
||||||
$buffer[] = $this->rowForInsert($row, $recordId, $deptId);
|
$buffer[] = $this->rowForInsert($row, $recordId, $deptId);
|
||||||
$done++;
|
$done++;
|
||||||
@@ -232,6 +272,137 @@ class WeightTestRunner
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否启用「底注随机」:每局付费抽奖从当前渠道底注配置中独立抽取 mult
|
||||||
|
*/
|
||||||
|
private function isAnteRandomMode(DiceRewardConfigRecord $record): bool
|
||||||
|
{
|
||||||
|
$snap = $this->normalizeTierWeightsSnapshot($record->tier_weights_snapshot ?? null);
|
||||||
|
if (is_array($snap) && !empty($snap['ante_random'])) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// 创建随机测试时主表 ante 固定为 0;快照丢失 ante_random 时仍按随机模式执行
|
||||||
|
$recordAnte = is_numeric($record->ante ?? null) ? (int) $record->ante : -1;
|
||||||
|
|
||||||
|
return $recordAnte === 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param mixed $snap
|
||||||
|
* @return array<string, mixed>|null
|
||||||
|
*/
|
||||||
|
private function normalizeTierWeightsSnapshot($snap): ?array
|
||||||
|
{
|
||||||
|
if (is_array($snap)) {
|
||||||
|
return $snap;
|
||||||
|
}
|
||||||
|
if (is_string($snap) && $snap !== '') {
|
||||||
|
$decoded = json_decode($snap, true);
|
||||||
|
|
||||||
|
return is_array($decoded) ? $decoded : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析本局有效底注(dice_ante_config.mult),禁止写入 0
|
||||||
|
*/
|
||||||
|
private function resolvePlayAnteMult(
|
||||||
|
int $deptId,
|
||||||
|
int $queuedAnte,
|
||||||
|
bool $isPaid,
|
||||||
|
bool $anteRandom,
|
||||||
|
int $recordAnte,
|
||||||
|
int $lastPaidPlayAnte
|
||||||
|
): int {
|
||||||
|
if ($isPaid) {
|
||||||
|
if ($anteRandom) {
|
||||||
|
return $this->pickRandomAnteMult($deptId);
|
||||||
|
}
|
||||||
|
if ($queuedAnte > 0) {
|
||||||
|
return $queuedAnte;
|
||||||
|
}
|
||||||
|
if ($recordAnte > 0) {
|
||||||
|
return $recordAnte;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->getMinAnteMult($deptId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 免费券(T5 链式):与触发付费局同底注,不得为 0
|
||||||
|
if ($queuedAnte > 0) {
|
||||||
|
return $queuedAnte;
|
||||||
|
}
|
||||||
|
if ($lastPaidPlayAnte > 0) {
|
||||||
|
return $lastPaidPlayAnte;
|
||||||
|
}
|
||||||
|
if ($recordAnte > 0) {
|
||||||
|
return $recordAnte;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->getMinAnteMult($deptId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 当前渠道底注配置中最小正数 mult
|
||||||
|
*/
|
||||||
|
private function getMinAnteMult(int $deptId): int
|
||||||
|
{
|
||||||
|
$mults = $this->listPositiveAnteMults($deptId);
|
||||||
|
|
||||||
|
return $mults !== [] ? $mults[0] : 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return list<int>
|
||||||
|
*/
|
||||||
|
private function listPositiveAnteMults(int $deptId): array
|
||||||
|
{
|
||||||
|
$anteQuery = DiceAnteConfig::field('mult')->order('mult', 'asc');
|
||||||
|
ConfigScopeEditHelper::applyDeptIdWhere($anteQuery, $deptId);
|
||||||
|
$rows = $anteQuery->select()->toArray();
|
||||||
|
$mults = [];
|
||||||
|
foreach ($rows as $row) {
|
||||||
|
$mult = (int) ($row['mult'] ?? 0);
|
||||||
|
if ($mult > 0) {
|
||||||
|
$mults[] = $mult;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $mults;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从当前渠道 dice_ante_config 中随机取一条 mult(仅 mult>0)
|
||||||
|
*/
|
||||||
|
private function pickRandomAnteMult(int $deptId): int
|
||||||
|
{
|
||||||
|
$mults = $this->listPositiveAnteMults($deptId);
|
||||||
|
if ($mults === []) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $mults[random_int(0, count($mults) - 1)];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从 tier_weights_snapshot 读取付费/免费档位权重快照
|
||||||
|
*/
|
||||||
|
private function resolveTierWeightsSnapshot(DiceRewardConfigRecord $record, string $side): ?array
|
||||||
|
{
|
||||||
|
$snap = $this->normalizeTierWeightsSnapshot($record->tier_weights_snapshot ?? null);
|
||||||
|
if ($snap === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
$weights = $snap[$side] ?? null;
|
||||||
|
if (! is_array($weights) || $weights === []) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $weights;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 解析本次测试渠道:优先读库字段,避免 ORM 字段缓存未含 dept_id 时读不到
|
* 解析本次测试渠道:优先读库字段,避免 ORM 字段缓存未含 dept_id 时读不到
|
||||||
*/
|
*/
|
||||||
@@ -316,6 +487,9 @@ class WeightTestRunner
|
|||||||
$out[$k] = $row[$k];
|
$out[$k] = $row[$k];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (array_key_exists('ante', $out) && (int) ($out['ante'] ?? 0) <= 0) {
|
||||||
|
$out['ante'] = $this->getMinAnteMult($bindDeptId > 0 ? $bindDeptId : $deptId);
|
||||||
|
}
|
||||||
return $out;
|
return $out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,13 @@ use plugin\saiadmin\basic\think\BaseModel as SaiBaseModel;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 大富翁模块模型基类:删除均为硬删除(物理删除)
|
* 大富翁模块模型基类:删除均为硬删除(物理删除)
|
||||||
|
*
|
||||||
|
* 注意:
|
||||||
|
* - 不要在此重写实例方法 delete(),否则与 trait/父类的 delete() 相互覆盖,
|
||||||
|
* 在调用 $this->force()->delete() 时会无限递归(force() 返回 $this),
|
||||||
|
* 导致内存爆栈、HTTP 500。
|
||||||
|
* - 物理删除一律通过静态 destroy() 入口(强制 $force=true)完成;
|
||||||
|
* SoftDelete::destroy() 内部会按硬删除分支执行。
|
||||||
*/
|
*/
|
||||||
abstract class DiceModel extends SaiBaseModel
|
abstract class DiceModel extends SaiBaseModel
|
||||||
{
|
{
|
||||||
@@ -17,9 +24,4 @@ abstract class DiceModel extends SaiBaseModel
|
|||||||
{
|
{
|
||||||
return parent::destroy($data, true);
|
return parent::destroy($data, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function delete(): bool
|
|
||||||
{
|
|
||||||
return $this->force()->delete();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,8 @@ use app\dice\model\DiceModel;
|
|||||||
*
|
*
|
||||||
* @property $id ID
|
* @property $id ID
|
||||||
* @property $name 名称
|
* @property $name 名称
|
||||||
* @property $remark 备注
|
* @property $remark 奖池名称(后台展示名)
|
||||||
|
* @property $config_note 配置备注
|
||||||
* @property $safety_line 安全线
|
* @property $safety_line 安全线
|
||||||
* @property $kill_enabled 是否启用杀分:0=关闭 1=开启
|
* @property $kill_enabled 是否启用杀分:0=关闭 1=开启
|
||||||
* @property $create_time 创建时间
|
* @property $create_time 创建时间
|
||||||
@@ -31,6 +32,9 @@ use app\dice\model\DiceModel;
|
|||||||
*/
|
*/
|
||||||
class DiceLotteryPoolConfig extends DiceModel
|
class DiceLotteryPoolConfig extends DiceModel
|
||||||
{
|
{
|
||||||
|
/** 玩家默认彩金池(新玩家关联;付费未杀分时运行时读取该池 T1–T5 权重) */
|
||||||
|
public const NAME_PLAYER_DEFAULT = 'playerDefault';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 数据表主键
|
* 数据表主键
|
||||||
* @var string
|
* @var string
|
||||||
@@ -43,6 +47,9 @@ class DiceLotteryPoolConfig extends DiceModel
|
|||||||
*/
|
*/
|
||||||
protected $table = 'dice_lottery_pool_config';
|
protected $table = 'dice_lottery_pool_config';
|
||||||
|
|
||||||
|
/** 列表/关联 JSON 附带奖池展示名 */
|
||||||
|
protected $append = ['display_name'];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 按名称与渠道查找奖池配置(一键测试等场景,避免命中其他渠道同名配置)
|
* 按名称与渠道查找奖池配置(一键测试等场景,避免命中其他渠道同名配置)
|
||||||
*/
|
*/
|
||||||
@@ -53,12 +60,68 @@ class DiceLotteryPoolConfig extends DiceModel
|
|||||||
return $query->find();
|
return $query->find();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否玩家默认模板池(name=playerDefault)
|
||||||
|
* 须用 getData()['name']:方法内 $this->name 会命中 ThinkORM 内部属性而非表字段,导致恒为 false
|
||||||
|
*/
|
||||||
|
public static function isPlayerDefaultPoolName($name): bool
|
||||||
|
{
|
||||||
|
return (string) $name === self::NAME_PLAYER_DEFAULT;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否玩家默认模板池(运行时按该池权重抽档,改池配置即对所有关联玩家生效)
|
||||||
|
*/
|
||||||
|
public function isPlayerDefaultTemplate(): bool
|
||||||
|
{
|
||||||
|
$data = $this->getData();
|
||||||
|
|
||||||
|
return self::isPlayerDefaultPoolName($data['name'] ?? '');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 后台展示用奖池名称:优先 remark,否则 name
|
||||||
|
*
|
||||||
|
* @param array<string, mixed>|self $row
|
||||||
|
*/
|
||||||
|
public static function displayLabel($row): string
|
||||||
|
{
|
||||||
|
// 禁止对模型实例 toArray():append display_name 会再次触发本方法,导致内存耗尽
|
||||||
|
if ($row instanceof self) {
|
||||||
|
$data = $row->getData();
|
||||||
|
$remark = trim((string) ($data['remark'] ?? ''));
|
||||||
|
if ($remark !== '') {
|
||||||
|
return $remark;
|
||||||
|
}
|
||||||
|
return trim((string) ($data['name'] ?? ''));
|
||||||
|
}
|
||||||
|
$remark = trim((string) ($row['remark'] ?? ''));
|
||||||
|
if ($remark !== '') {
|
||||||
|
return $remark;
|
||||||
|
}
|
||||||
|
return trim((string) ($row['name'] ?? ''));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getDisplayNameAttr(): string
|
||||||
|
{
|
||||||
|
$data = $this->getData();
|
||||||
|
$remark = trim((string) ($data['remark'] ?? ''));
|
||||||
|
if ($remark !== '') {
|
||||||
|
return $remark;
|
||||||
|
}
|
||||||
|
return trim((string) ($data['name'] ?? ''));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 名称 搜索
|
* 名称 搜索
|
||||||
*/
|
*/
|
||||||
public function searchNameAttr($query, $value)
|
public function searchNameAttr($query, $value)
|
||||||
{
|
{
|
||||||
$query->where('name', 'like', '%'.$value.'%');
|
$like = '%' . $value . '%';
|
||||||
|
$query->where(function ($q) use ($like) {
|
||||||
|
$q->where('name', 'like', $like)
|
||||||
|
->whereOr('remark', 'like', $like);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ use think\model\relation\BelongsTo;
|
|||||||
* @property $use_coins 消耗平台币(兼容字段:付费局=paid_amount,免费局=0)
|
* @property $use_coins 消耗平台币(兼容字段:付费局=paid_amount,免费局=0)
|
||||||
* @property $direction 方向:0=顺时针,1=逆时针
|
* @property $direction 方向:0=顺时针,1=逆时针
|
||||||
* @property $reward_tier 中奖档位:T1,T2,T3,T4,T5,BIGWIN
|
* @property $reward_tier 中奖档位:T1,T2,T3,T4,T5,BIGWIN
|
||||||
|
* @property $remark 备注(如惩罚格余额不足)
|
||||||
* @property $lottery_id 奖池
|
* @property $lottery_id 奖池
|
||||||
* @property $start_index 起始索引
|
* @property $start_index 起始索引
|
||||||
* @property $target_index 结束索引
|
* @property $target_index 结束索引
|
||||||
@@ -87,13 +88,17 @@ class DicePlayRecord extends DiceModel
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 按彩金池配置名称模糊(diceLotteryPoolConfig.name) */
|
/** 按彩金池奖池名称或内部标识模糊搜索 */
|
||||||
public function searchLotteryConfigNameAttr($query, $value)
|
public function searchLotteryConfigNameAttr($query, $value)
|
||||||
{
|
{
|
||||||
if ($value === '' || $value === null) {
|
if ($value === '' || $value === null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
$ids = DiceLotteryPoolConfig::where('name', 'like', '%' . $value . '%')->column('id');
|
$like = '%' . $value . '%';
|
||||||
|
$ids = DiceLotteryPoolConfig::where(function ($q) use ($like) {
|
||||||
|
$q->where('name', 'like', $like)
|
||||||
|
->whereOr('remark', 'like', $like);
|
||||||
|
})->column('id');
|
||||||
if (!empty($ids)) {
|
if (!empty($ids)) {
|
||||||
$query->whereIn('lottery_config_id', $ids);
|
$query->whereIn('lottery_config_id', $ids);
|
||||||
} else {
|
} else {
|
||||||
@@ -281,4 +286,20 @@ class DicePlayRecord extends DiceModel
|
|||||||
$query->where('roll_number', '<=', $value);
|
$query->where('roll_number', '<=', $value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 创建时间起始 */
|
||||||
|
public function searchCreateTimeMinAttr($query, $value)
|
||||||
|
{
|
||||||
|
if ($value !== '' && $value !== null) {
|
||||||
|
$query->where('create_time', '>=', $value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 创建时间结束 */
|
||||||
|
public function searchCreateTimeMaxAttr($query, $value)
|
||||||
|
{
|
||||||
|
if ($value !== '' && $value !== null) {
|
||||||
|
$query->where('create_time', '<=', $value);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -73,6 +73,15 @@ class DicePlayRecordTest extends DiceModel
|
|||||||
return $this->belongsTo(DiceRewardConfigRecord::class, 'reward_config_record_id', 'id');
|
return $this->belongsTo(DiceRewardConfigRecord::class, 'reward_config_record_id', 'id');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 彩金池配置 id(关联 dice_lottery_pool_config.id) */
|
||||||
|
public function searchLotteryConfigIdAttr($query, $value): void
|
||||||
|
{
|
||||||
|
if ($value === '' || $value === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$query->where('lottery_config_id', '=', (int) $value);
|
||||||
|
}
|
||||||
|
|
||||||
/** 抽奖类型 0=付费 1=免费 */
|
/** 抽奖类型 0=付费 1=免费 */
|
||||||
public function searchLotteryTypeAttr($query, $value)
|
public function searchLotteryTypeAttr($query, $value)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
// +----------------------------------------------------------------------
|
// +----------------------------------------------------------------------
|
||||||
namespace app\dice\model\player;
|
namespace app\dice\model\player;
|
||||||
|
|
||||||
|
use app\dice\helper\AdminScopeHelper;
|
||||||
use app\dice\model\DiceModel;
|
use app\dice\model\DiceModel;
|
||||||
use app\dice\model\lottery_pool_config\DiceLotteryPoolConfig;
|
use app\dice\model\lottery_pool_config\DiceLotteryPoolConfig;
|
||||||
|
|
||||||
@@ -84,26 +85,61 @@ class DicePlayer extends DiceModel
|
|||||||
if ($name === null || $name === '') {
|
if ($name === null || $name === '') {
|
||||||
$model->setAttr('name', $uid);
|
$model->setAttr('name', $uid);
|
||||||
}
|
}
|
||||||
// 创建玩家时:未指定则自动保存 lottery_config_id 为 DiceLotteryPoolConfig name=default 的 id,没有则为 0
|
// 创建玩家时:未指定则关联 name=playerDefault(玩家默认彩金池),没有则为 0
|
||||||
try {
|
try {
|
||||||
$lotteryConfigId = $model->getAttr('lottery_config_id');
|
$lotteryConfigId = $model->getAttr('lottery_config_id');
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
$lotteryConfigId = null;
|
$lotteryConfigId = null;
|
||||||
}
|
}
|
||||||
if ($lotteryConfigId === null || $lotteryConfigId === '' || (int) $lotteryConfigId === 0) {
|
if ($lotteryConfigId === null || $lotteryConfigId === '' || (int) $lotteryConfigId === 0) {
|
||||||
$config = DiceLotteryPoolConfig::where('name', 'default')->find();
|
$config = self::findPlayerDefaultLotteryConfigForPlayer($model);
|
||||||
$model->setAttr('lottery_config_id', $config ? (int) $config->id : 0);
|
$model->setAttr('lottery_config_id', $config ? (int) $config->id : 0);
|
||||||
}
|
}
|
||||||
// 彩金池权重默认取 name=default 的奖池配置
|
// 展示用权重:从玩家关联的彩金池复制(playerDefault 在抽奖时仍实时读池配置)
|
||||||
self::setDefaultWeightsFromLotteryConfig($model);
|
self::setDefaultWeightsFromLotteryConfig($model);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 从 DiceLotteryPoolConfig name=default 取 t1_weight~t5_weight 作为玩家未设置时的默认值
|
* 按玩家所属渠道查找玩家默认彩金池(name=playerDefault)
|
||||||
|
*/
|
||||||
|
protected static function findPlayerDefaultLotteryConfigForPlayer(DicePlayer $model): ?DiceLotteryPoolConfig
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$deptId = $model->getAttr('dept_id');
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
$deptId = null;
|
||||||
|
}
|
||||||
|
$normalizedDeptId = AdminScopeHelper::resolvePlayerConfigDeptId($model);
|
||||||
|
if ($deptId !== null && $deptId !== '' && (int) $deptId > 0) {
|
||||||
|
$normalizedDeptId = (int) $deptId;
|
||||||
|
}
|
||||||
|
$config = DiceLotteryPoolConfig::findByNameForDept(
|
||||||
|
DiceLotteryPoolConfig::NAME_PLAYER_DEFAULT,
|
||||||
|
$normalizedDeptId
|
||||||
|
);
|
||||||
|
if ($config) {
|
||||||
|
return $config;
|
||||||
|
}
|
||||||
|
return DiceLotteryPoolConfig::findByNameForDept('default', $normalizedDeptId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从玩家关联彩金池(或 playerDefault / default)取 t1_weight~t5_weight 作为未设置时的默认值
|
||||||
*/
|
*/
|
||||||
protected static function setDefaultWeightsFromLotteryConfig(DicePlayer $model): void
|
protected static function setDefaultWeightsFromLotteryConfig(DicePlayer $model): void
|
||||||
{
|
{
|
||||||
$config = DiceLotteryPoolConfig::where('name', 'default')->find();
|
$config = null;
|
||||||
|
try {
|
||||||
|
$lotteryConfigId = (int) $model->getAttr('lottery_config_id');
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
$lotteryConfigId = 0;
|
||||||
|
}
|
||||||
|
if ($lotteryConfigId > 0) {
|
||||||
|
$config = DiceLotteryPoolConfig::find($lotteryConfigId);
|
||||||
|
}
|
||||||
|
if (!$config) {
|
||||||
|
$config = self::findPlayerDefaultLotteryConfigForPlayer($model);
|
||||||
|
}
|
||||||
if (!$config) {
|
if (!$config) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -188,6 +224,26 @@ class DicePlayer extends DiceModel
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 注册时间起始 搜索
|
||||||
|
*/
|
||||||
|
public function searchCreateTimeMinAttr($query, $value)
|
||||||
|
{
|
||||||
|
if ($value !== '' && $value !== null) {
|
||||||
|
$query->where('create_time', '>=', $value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 注册时间结束 搜索
|
||||||
|
*/
|
||||||
|
public function searchCreateTimeMaxAttr($query, $value)
|
||||||
|
{
|
||||||
|
if ($value !== '' && $value !== null) {
|
||||||
|
$query->where('create_time', '<=', $value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 关联彩金池配置
|
* 关联彩金池配置
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ namespace app\dice\service;
|
|||||||
|
|
||||||
use app\dice\helper\AdminScopeHelper;
|
use app\dice\helper\AdminScopeHelper;
|
||||||
use app\dice\logic\reward\DiceRewardLogic;
|
use app\dice\logic\reward\DiceRewardLogic;
|
||||||
|
use app\dice\model\lottery_pool_config\DiceLotteryPoolConfig;
|
||||||
use app\dice\model\reward\DiceReward;
|
use app\dice\model\reward\DiceReward;
|
||||||
use app\dice\model\reward_config\DiceRewardConfig;
|
use app\dice\model\reward_config\DiceRewardConfig;
|
||||||
use plugin\saiadmin\app\model\system\SystemDept;
|
use plugin\saiadmin\app\model\system\SystemDept;
|
||||||
@@ -54,6 +55,22 @@ class DiceChannelConfigService
|
|||||||
'dice_reward_config_record' => ['label' => '权重测试记录', 'group' => 'records'],
|
'dice_reward_config_record' => ['label' => '权重测试记录', 'group' => 'records'],
|
||||||
];
|
];
|
||||||
|
|
||||||
|
/** 关联数据删除顺序:先删流水/记录,再删玩家,最后删配置 */
|
||||||
|
private const DELETE_TABLE_ORDER = [
|
||||||
|
'dice_play_record',
|
||||||
|
'dice_play_record_test',
|
||||||
|
'dice_player_wallet_record',
|
||||||
|
'dice_player_ticket_record',
|
||||||
|
'dice_reward_config_record',
|
||||||
|
'dice_player',
|
||||||
|
'dice_reward',
|
||||||
|
'dice_reward_config',
|
||||||
|
'dice_config',
|
||||||
|
'dice_ante_config',
|
||||||
|
'dice_lottery_pool_config',
|
||||||
|
'dice_game',
|
||||||
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 默认模板 dept_id 统一为 0,并为固定 id 的配置表建立 (dept_id, id) 唯一约束
|
* 默认模板 dept_id 统一为 0,并为固定 id 的配置表建立 (dept_id, id) 唯一约束
|
||||||
*/
|
*/
|
||||||
@@ -193,9 +210,99 @@ class DiceChannelConfigService
|
|||||||
}
|
}
|
||||||
$this->ensureRewardReferenceForDept($deptId);
|
$this->ensureRewardReferenceForDept($deptId);
|
||||||
DiceRewardConfig::refreshCache($deptId);
|
DiceRewardConfig::refreshCache($deptId);
|
||||||
|
$this->ensurePlayerDefaultPoolForDept($deptId);
|
||||||
|
$this->migratePlayersDefaultPoolToPlayerDefault($deptId);
|
||||||
return $result;
|
return $result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 为渠道补齐玩家默认彩金池 name=playerDefault(权重复制自 default)
|
||||||
|
*/
|
||||||
|
public function ensurePlayerDefaultPoolForDept(int $deptId): ?int
|
||||||
|
{
|
||||||
|
if (!$this->tableHasColumn('dice_lottery_pool_config', 'dept_id')) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
$exists = Db::table('dice_lottery_pool_config')
|
||||||
|
->where('dept_id', $deptId)
|
||||||
|
->where('name', DiceLotteryPoolConfig::NAME_PLAYER_DEFAULT)
|
||||||
|
->count();
|
||||||
|
if ($exists > 0) {
|
||||||
|
return (int) Db::table('dice_lottery_pool_config')
|
||||||
|
->where('dept_id', $deptId)
|
||||||
|
->where('name', DiceLotteryPoolConfig::NAME_PLAYER_DEFAULT)
|
||||||
|
->value('id');
|
||||||
|
}
|
||||||
|
$defaultRow = Db::table('dice_lottery_pool_config')
|
||||||
|
->where('dept_id', $deptId)
|
||||||
|
->where('name', 'default')
|
||||||
|
->find();
|
||||||
|
if (!$defaultRow) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
$defaultRow = (array) $defaultRow;
|
||||||
|
unset($defaultRow['id'], $defaultRow['row_id'], $defaultRow['create_time'], $defaultRow['update_time'], $defaultRow['delete_time']);
|
||||||
|
$defaultRow['name'] = DiceLotteryPoolConfig::NAME_PLAYER_DEFAULT;
|
||||||
|
$defaultRow['remark'] = '默认';
|
||||||
|
$defaultRow['safety_line'] = 0;
|
||||||
|
$defaultRow['kill_enabled'] = 0;
|
||||||
|
$defaultRow['profit_amount'] = 0;
|
||||||
|
$defaultRow['dept_id'] = $deptId;
|
||||||
|
return (int) Db::table('dice_lottery_pool_config')->insertGetId($defaultRow);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将仍关联 name=default 的玩家改为关联 playerDefault(杀分逻辑仍用 default 池)
|
||||||
|
*/
|
||||||
|
public function migratePlayersDefaultPoolToPlayerDefault(int $deptId): int
|
||||||
|
{
|
||||||
|
if (!$this->tableHasColumn('dice_player', 'dept_id')
|
||||||
|
|| !$this->tableHasColumn('dice_player', 'lottery_config_id')) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
$playerDefaultId = Db::table('dice_lottery_pool_config')
|
||||||
|
->where('dept_id', $deptId)
|
||||||
|
->where('name', DiceLotteryPoolConfig::NAME_PLAYER_DEFAULT)
|
||||||
|
->value('id');
|
||||||
|
if (!$playerDefaultId) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
$defaultPoolId = Db::table('dice_lottery_pool_config')
|
||||||
|
->where('dept_id', $deptId)
|
||||||
|
->where('name', 'default')
|
||||||
|
->value('id');
|
||||||
|
if (!$defaultPoolId) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return Db::table('dice_player')
|
||||||
|
->where('dept_id', $deptId)
|
||||||
|
->where('lottery_config_id', (int) $defaultPoolId)
|
||||||
|
->update(['lottery_config_id' => (int) $playerDefaultId]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 为全部渠道与默认模板补齐 playerDefault 奖池并迁移玩家关联
|
||||||
|
*/
|
||||||
|
public function ensurePlayerDefaultPoolsAllChannels(): array
|
||||||
|
{
|
||||||
|
$deptIds = [AdminScopeHelper::DEFAULT_TEMPLATE_DEPT];
|
||||||
|
foreach (SystemDept::column('id') as $id) {
|
||||||
|
$id = (int) $id;
|
||||||
|
if ($id > 0) {
|
||||||
|
$deptIds[] = $id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$deptIds = array_values(array_unique($deptIds));
|
||||||
|
$summary = [];
|
||||||
|
foreach ($deptIds as $deptId) {
|
||||||
|
$summary[$deptId] = [
|
||||||
|
'pool_id' => $this->ensurePlayerDefaultPoolForDept($deptId),
|
||||||
|
'players_migrated' => $this->migratePlayersDefaultPoolToPlayerDefault($deptId),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
return $summary;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 按业务 id 从默认模板补齐配置(dice_config / dice_reward_config)
|
* 按业务 id 从默认模板补齐配置(dice_config / dice_reward_config)
|
||||||
*/
|
*/
|
||||||
@@ -281,6 +388,7 @@ class DiceChannelConfigService
|
|||||||
}
|
}
|
||||||
$summary[$deptId] = $this->copyDefaultConfigToDept($deptId);
|
$summary[$deptId] = $this->copyDefaultConfigToDept($deptId);
|
||||||
}
|
}
|
||||||
|
$summary['_player_default_pools'] = $this->ensurePlayerDefaultPoolsAllChannels();
|
||||||
return $summary;
|
return $summary;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -417,21 +525,54 @@ class DiceChannelConfigService
|
|||||||
if ($userCount > 0) {
|
if ($userCount > 0) {
|
||||||
throw new ApiException('This channel has users, please delete or transfer them first');
|
throw new ApiException('This channel has users, please delete or transfer them first');
|
||||||
}
|
}
|
||||||
$allowed = array_keys(self::RELATION_TABLES);
|
$tablesToDelete = $this->sortTablesForDelete($deleteTables);
|
||||||
foreach ($deleteTables as $table) {
|
Db::startTrans();
|
||||||
if (!in_array($table, $allowed, true)) {
|
try {
|
||||||
continue;
|
foreach ($tablesToDelete as $table) {
|
||||||
}
|
|
||||||
if (!$this->tableHasColumn($table, 'dept_id')) {
|
if (!$this->tableHasColumn($table, 'dept_id')) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
Db::table($table)->where('dept_id', $deptId)->delete();
|
Db::table($table)->where('dept_id', $deptId)->delete();
|
||||||
}
|
}
|
||||||
|
Db::name('sa_system_role_dept')->where('dept_id', $deptId)->delete();
|
||||||
SystemDept::destroy($deptId, true);
|
SystemDept::destroy($deptId, true);
|
||||||
|
Db::commit();
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
Db::rollback();
|
||||||
|
throw new ApiException('Channel delete failed: ' . $e->getMessage());
|
||||||
|
}
|
||||||
DiceRewardConfig::refreshCache($deptId);
|
DiceRewardConfig::refreshCache($deptId);
|
||||||
DiceReward::refreshCache($deptId);
|
DiceReward::refreshCache($deptId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按依赖顺序排列待删表(勾选顺序无关)
|
||||||
|
*
|
||||||
|
* @param array<int, string> $deleteTables
|
||||||
|
* @return array<int, string>
|
||||||
|
*/
|
||||||
|
private function sortTablesForDelete(array $deleteTables): array
|
||||||
|
{
|
||||||
|
$allowed = array_keys(self::RELATION_TABLES);
|
||||||
|
$picked = [];
|
||||||
|
foreach ($deleteTables as $table) {
|
||||||
|
if (is_string($table) && in_array($table, $allowed, true)) {
|
||||||
|
$picked[$table] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$ordered = [];
|
||||||
|
foreach (self::DELETE_TABLE_ORDER as $table) {
|
||||||
|
if (isset($picked[$table])) {
|
||||||
|
$ordered[] = $table;
|
||||||
|
unset($picked[$table]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
foreach (array_keys($picked) as $table) {
|
||||||
|
$ordered[] = $table;
|
||||||
|
}
|
||||||
|
return $ordered;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return array<int, array<string, mixed>>
|
* @return array<int, array<string, mixed>>
|
||||||
*/
|
*/
|
||||||
@@ -477,7 +618,9 @@ class DiceChannelConfigService
|
|||||||
if ($deptId === null || $deptId === AdminScopeHelper::DEFAULT_TEMPLATE_DEPT) {
|
if ($deptId === null || $deptId === AdminScopeHelper::DEFAULT_TEMPLATE_DEPT) {
|
||||||
$templateId = AdminScopeHelper::DEFAULT_TEMPLATE_DEPT;
|
$templateId = AdminScopeHelper::DEFAULT_TEMPLATE_DEPT;
|
||||||
$query->where(function ($q) use ($templateId) {
|
$query->where(function ($q) use ($templateId) {
|
||||||
$q->where('dept_id', $templateId)->whereOr('dept_id', 'null');
|
$q->where('dept_id', $templateId)->whereOr(function ($sub) {
|
||||||
|
$sub->whereNull('dept_id');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
$query->where('dept_id', $deptId);
|
$query->where('dept_id', $deptId);
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ class DiceLotteryPoolConfigValidate extends BaseValidate
|
|||||||
*/
|
*/
|
||||||
protected $rule = [
|
protected $rule = [
|
||||||
'name' => 'require',
|
'name' => 'require',
|
||||||
|
'remark' => 'max:200',
|
||||||
|
'config_note' => 'max:500',
|
||||||
't1_weight' => 'require',
|
't1_weight' => 'require',
|
||||||
't2_weight' => 'require',
|
't2_weight' => 'require',
|
||||||
't3_weight' => 'require',
|
't3_weight' => 'require',
|
||||||
@@ -43,6 +45,8 @@ class DiceLotteryPoolConfigValidate extends BaseValidate
|
|||||||
protected $scene = [
|
protected $scene = [
|
||||||
'save' => [
|
'save' => [
|
||||||
'name',
|
'name',
|
||||||
|
'remark',
|
||||||
|
'config_note',
|
||||||
't1_weight',
|
't1_weight',
|
||||||
't2_weight',
|
't2_weight',
|
||||||
't3_weight',
|
't3_weight',
|
||||||
@@ -51,6 +55,8 @@ class DiceLotteryPoolConfigValidate extends BaseValidate
|
|||||||
],
|
],
|
||||||
'update' => [
|
'update' => [
|
||||||
'name',
|
'name',
|
||||||
|
'remark',
|
||||||
|
'config_note',
|
||||||
't1_weight',
|
't1_weight',
|
||||||
't2_weight',
|
't2_weight',
|
||||||
't3_weight',
|
't3_weight',
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ return [
|
|||||||
'session_username_prefix' => env('API_SESSION_USERNAME_PREFIX', 'api:user:session:'),
|
'session_username_prefix' => env('API_SESSION_USERNAME_PREFIX', 'api:user:session:'),
|
||||||
// 登录会话过期时间(秒),默认 7 天
|
// 登录会话过期时间(秒),默认 7 天
|
||||||
'session_expire' => (int) env('API_SESSION_EXPIRE', 604800),
|
'session_expire' => (int) env('API_SESSION_EXPIRE', 604800),
|
||||||
|
// 平台对接请求头 api-key(/api/v1/* 必填,与客户端请求头 api-key 一致)
|
||||||
|
'platform_api_key' => env('API_KEY', ''),
|
||||||
// auth-token 签名密钥(与客户端约定,用于 /api/authToken 的 signature 校验,必填)
|
// auth-token 签名密钥(与客户端约定,用于 /api/authToken 的 signature 校验,必填)
|
||||||
'auth_token_secret' => env('API_AUTH_TOKEN_SECRET', ''),
|
'auth_token_secret' => env('API_AUTH_TOKEN_SECRET', ''),
|
||||||
// auth-token 时间戳允许误差(秒),防重放,默认 300 秒
|
// auth-token 时间戳允许误差(秒),防重放,默认 300 秒
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
|
|
||||||
use Webman\Route;
|
use Webman\Route;
|
||||||
use app\api\middleware\ApiAccessLogMiddleware;
|
use app\api\middleware\ApiAccessLogMiddleware;
|
||||||
|
use app\api\middleware\ApiKeyMiddleware;
|
||||||
use app\api\middleware\AuthTokenMiddleware;
|
use app\api\middleware\AuthTokenMiddleware;
|
||||||
use app\api\middleware\TokenMiddleware;
|
use app\api\middleware\TokenMiddleware;
|
||||||
|
|
||||||
@@ -22,9 +23,10 @@ Route::group('/api/v1', function () {
|
|||||||
Route::any('/authToken', [app\api\controller\v1\AuthTokenController::class, 'index']);
|
Route::any('/authToken', [app\api\controller\v1\AuthTokenController::class, 'index']);
|
||||||
})->middleware([
|
})->middleware([
|
||||||
ApiAccessLogMiddleware::class,
|
ApiAccessLogMiddleware::class,
|
||||||
|
ApiKeyMiddleware::class,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// 平台 v1 接口:需在请求头携带 auth-token
|
// 平台 v1 接口:需在请求头携带 api-key、auth-token
|
||||||
Route::group('/api/v1', function () {
|
Route::group('/api/v1', function () {
|
||||||
Route::any('/getGameList', [app\api\controller\v1\GameController::class, 'getGameList']);
|
Route::any('/getGameList', [app\api\controller\v1\GameController::class, 'getGameList']);
|
||||||
Route::any('/getGameHall', [app\api\controller\v1\GameController::class, 'getGameHall']);
|
Route::any('/getGameHall', [app\api\controller\v1\GameController::class, 'getGameHall']);
|
||||||
@@ -36,6 +38,7 @@ Route::group('/api/v1', function () {
|
|||||||
Route::any('/setPlayerWallet', [app\api\controller\v1\GameController::class, 'setPlayerWallet']);
|
Route::any('/setPlayerWallet', [app\api\controller\v1\GameController::class, 'setPlayerWallet']);
|
||||||
})->middleware([
|
})->middleware([
|
||||||
ApiAccessLogMiddleware::class,
|
ApiAccessLogMiddleware::class,
|
||||||
|
ApiKeyMiddleware::class,
|
||||||
AuthTokenMiddleware::class,
|
AuthTokenMiddleware::class,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|||||||
84
server/db/audit_channel_config.php
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* 审计各渠道游戏配置是否已从默认模板实例化
|
||||||
|
* 用法:php server/db/audit_channel_config.php [--fix]
|
||||||
|
*/
|
||||||
|
require_once __DIR__ . '/../vendor/autoload.php';
|
||||||
|
require_once __DIR__ . '/../support/bootstrap.php';
|
||||||
|
|
||||||
|
use app\dice\helper\AdminScopeHelper;
|
||||||
|
use app\dice\service\DiceChannelConfigService;
|
||||||
|
use plugin\saiadmin\app\model\system\SystemDept;
|
||||||
|
use support\think\Db;
|
||||||
|
|
||||||
|
$fix = in_array('--fix', $argv ?? [], true);
|
||||||
|
$templateId = AdminScopeHelper::DEFAULT_TEMPLATE_DEPT;
|
||||||
|
$tables = [
|
||||||
|
'dice_config',
|
||||||
|
'dice_ante_config',
|
||||||
|
'dice_lottery_pool_config',
|
||||||
|
'dice_reward_config',
|
||||||
|
'dice_game',
|
||||||
|
'dice_reward',
|
||||||
|
];
|
||||||
|
|
||||||
|
$templateCounts = [];
|
||||||
|
foreach ($tables as $table) {
|
||||||
|
$templateCounts[$table] = (int) Db::table($table)
|
||||||
|
->where(function ($q) use ($templateId) {
|
||||||
|
$q->where('dept_id', $templateId)->whereOr('dept_id', null);
|
||||||
|
})
|
||||||
|
->count();
|
||||||
|
}
|
||||||
|
|
||||||
|
$depts = SystemDept::where('id', '>', 0)->column('id');
|
||||||
|
echo "========== 渠道配置实例化审计 ==========\n";
|
||||||
|
echo "默认模板 dept_id={$templateId} 行数:\n";
|
||||||
|
foreach ($templateCounts as $table => $cnt) {
|
||||||
|
echo " {$table}: {$cnt}\n";
|
||||||
|
}
|
||||||
|
echo "\n";
|
||||||
|
|
||||||
|
$missing = [];
|
||||||
|
foreach ($depts as $deptId) {
|
||||||
|
$deptId = (int) $deptId;
|
||||||
|
if ($deptId <= 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$issues = [];
|
||||||
|
foreach ($tables as $table) {
|
||||||
|
$expected = $templateCounts[$table];
|
||||||
|
if ($expected <= 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$actual = (int) Db::table($table)->where('dept_id', $deptId)->count();
|
||||||
|
if ($actual < $expected) {
|
||||||
|
$issues[] = "{$table}: {$actual}/{$expected}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($issues !== []) {
|
||||||
|
$missing[$deptId] = $issues;
|
||||||
|
echo "渠道 {$deptId} 不完整 → " . implode(', ', $issues) . "\n";
|
||||||
|
} else {
|
||||||
|
echo "渠道 {$deptId} OK\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($missing === []) {
|
||||||
|
echo "\n全部渠道配置已实例化。\n";
|
||||||
|
exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$fix) {
|
||||||
|
echo "\n存在缺失。执行 php server/db/audit_channel_config.php --fix 可自动补齐。\n";
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "\n开始补齐...\n";
|
||||||
|
$service = new DiceChannelConfigService();
|
||||||
|
$summary = $service->syncAllChannelsFromDefault();
|
||||||
|
foreach ($summary as $deptId => $info) {
|
||||||
|
$copied = implode(',', $info['copied_tables'] ?? []);
|
||||||
|
echo "渠道 {$deptId}: 新增表 [{$copied}] 补齐行 " . ($info['merged_rows'] ?? 0) . "\n";
|
||||||
|
}
|
||||||
|
echo "补齐完成,请重新运行审计确认。\n";
|
||||||
16
server/db/check_reward_config.php
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
define('BASE_PATH', dirname(__DIR__));
|
||||||
|
require BASE_PATH . '/vendor/autoload.php';
|
||||||
|
\Dotenv\Dotenv::createUnsafeMutable(BASE_PATH)->load();
|
||||||
|
\Webman\Config::load(BASE_PATH . '/config', ['route', 'plugin']);
|
||||||
|
\Webman\ThinkOrm\ThinkOrm::start(null);
|
||||||
|
use support\think\Db;
|
||||||
|
$depts = Db::table('sa_system_dept')->column('id');
|
||||||
|
array_unshift($depts, 0);
|
||||||
|
foreach ($depts as $d) {
|
||||||
|
$c = Db::table('dice_reward_config')->where('dept_id', $d)->count();
|
||||||
|
$r = Db::table('dice_reward')->where('dept_id', $d)->count();
|
||||||
|
$nonBig = Db::table('dice_reward_config')->where('dept_id', $d)->where('tier', '<>', 'BIGWIN')->count();
|
||||||
|
echo "dept {$d}: reward_config={$c}, non_bigwin={$nonBig}, reward={$r}\n";
|
||||||
|
}
|
||||||
11
server/db/count_config_tables.php
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
define('BASE_PATH', dirname(__DIR__));
|
||||||
|
require BASE_PATH . '/vendor/autoload.php';
|
||||||
|
\Dotenv\Dotenv::createUnsafeMutable(BASE_PATH)->load();
|
||||||
|
\Webman\ThinkOrm\ThinkOrm::start(null);
|
||||||
|
use support\think\Db;
|
||||||
|
|
||||||
|
foreach (['dice_config', 'dice_ante_config', 'dice_game', 'dice_lottery_pool_config'] as $table) {
|
||||||
|
echo "{$table} dept1123: " . Db::table($table)->where('dept_id', 1123)->count() . "\n";
|
||||||
|
}
|
||||||
52
server/db/debug_check_table_type.php
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询指定表/视图的类型(BASE TABLE / VIEW)。
|
||||||
|
*
|
||||||
|
* 用法(在 server 目录执行):
|
||||||
|
* php db/debug_check_table_type.php bet_order_view
|
||||||
|
*/
|
||||||
|
|
||||||
|
require_once __DIR__ . '/../vendor/autoload.php';
|
||||||
|
|
||||||
|
if (class_exists(\Dotenv\Dotenv::class) && is_file(dirname(__DIR__) . '/.env')) {
|
||||||
|
if (method_exists(\Dotenv\Dotenv::class, 'createUnsafeMutable')) {
|
||||||
|
\Dotenv\Dotenv::createUnsafeMutable(dirname(__DIR__))->load();
|
||||||
|
} else {
|
||||||
|
\Dotenv\Dotenv::createMutable(dirname(__DIR__))->load();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$table = $argv[1] ?? '';
|
||||||
|
$table = trim((string) $table);
|
||||||
|
if ($table === '') {
|
||||||
|
fwrite(STDERR, "Missing table name.\n");
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
$host = getenv('DB_HOST') ?: '127.0.0.1';
|
||||||
|
$port = getenv('DB_PORT') ?: '3306';
|
||||||
|
$dbName = getenv('DB_NAME') ?: '';
|
||||||
|
$user = getenv('DB_USER') ?: '';
|
||||||
|
$pass = getenv('DB_PASSWORD') ?: '';
|
||||||
|
|
||||||
|
$dsn = "mysql:host={$host};port={$port};dbname={$dbName};charset=utf8mb4";
|
||||||
|
$pdo = new PDO($dsn, $user, $pass, [
|
||||||
|
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare(
|
||||||
|
"SELECT TABLE_NAME, TABLE_TYPE
|
||||||
|
FROM information_schema.TABLES
|
||||||
|
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = :name"
|
||||||
|
);
|
||||||
|
$stmt->execute(['name' => $table]);
|
||||||
|
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
if (!$row) {
|
||||||
|
echo "NOT_FOUND\t{$table}\n";
|
||||||
|
exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
echo $row['TABLE_NAME'] . "\t" . $row['TABLE_TYPE'] . "\n";
|
||||||
|
|
||||||
47
server/db/debug_list_view_backup_objects.php
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 列出当前数据库中所有 *__view_backup 对象,用于排查宝塔备份报错。
|
||||||
|
*
|
||||||
|
* 用法(在 server 目录执行):
|
||||||
|
* php db/debug_list_view_backup_objects.php
|
||||||
|
*/
|
||||||
|
|
||||||
|
require_once __DIR__ . '/../vendor/autoload.php';
|
||||||
|
|
||||||
|
if (class_exists(\Dotenv\Dotenv::class) && is_file(dirname(__DIR__) . '/.env')) {
|
||||||
|
if (method_exists(\Dotenv\Dotenv::class, 'createUnsafeMutable')) {
|
||||||
|
\Dotenv\Dotenv::createUnsafeMutable(dirname(__DIR__))->load();
|
||||||
|
} else {
|
||||||
|
\Dotenv\Dotenv::createMutable(dirname(__DIR__))->load();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$host = getenv('DB_HOST') ?: '127.0.0.1';
|
||||||
|
$port = getenv('DB_PORT') ?: '3306';
|
||||||
|
$dbName = getenv('DB_NAME') ?: '';
|
||||||
|
$user = getenv('DB_USER') ?: '';
|
||||||
|
$pass = getenv('DB_PASSWORD') ?: '';
|
||||||
|
|
||||||
|
$dsn = "mysql:host={$host};port={$port};dbname={$dbName};charset=utf8mb4";
|
||||||
|
$pdo = new PDO($dsn, $user, $pass, [
|
||||||
|
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$sql = "SELECT TABLE_NAME, TABLE_TYPE
|
||||||
|
FROM information_schema.TABLES
|
||||||
|
WHERE TABLE_SCHEMA = DATABASE()
|
||||||
|
AND TABLE_NAME LIKE '%\\_\\_view\\_backup'
|
||||||
|
ORDER BY TABLE_NAME";
|
||||||
|
|
||||||
|
$rows = $pdo->query($sql)->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
if (!$rows) {
|
||||||
|
echo "No *__view_backup objects found.\n";
|
||||||
|
exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($rows as $row) {
|
||||||
|
echo $row['TABLE_NAME'] . "\t" . $row['TABLE_TYPE'] . "\n";
|
||||||
|
}
|
||||||
|
|
||||||
46
server/db/debug_list_views_and_definers.php
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 列出当前数据库中的 VIEW 及其 DEFINER,用于排查宝塔备份“缺少表 xxx__view_backup”。
|
||||||
|
*
|
||||||
|
* 用法(在 server 目录执行):
|
||||||
|
* php db/debug_list_views_and_definers.php
|
||||||
|
*/
|
||||||
|
|
||||||
|
require_once __DIR__ . '/../vendor/autoload.php';
|
||||||
|
|
||||||
|
if (class_exists(\Dotenv\Dotenv::class) && is_file(dirname(__DIR__) . '/.env')) {
|
||||||
|
if (method_exists(\Dotenv\Dotenv::class, 'createUnsafeMutable')) {
|
||||||
|
\Dotenv\Dotenv::createUnsafeMutable(dirname(__DIR__))->load();
|
||||||
|
} else {
|
||||||
|
\Dotenv\Dotenv::createMutable(dirname(__DIR__))->load();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$host = getenv('DB_HOST') ?: '127.0.0.1';
|
||||||
|
$port = getenv('DB_PORT') ?: '3306';
|
||||||
|
$dbName = getenv('DB_NAME') ?: '';
|
||||||
|
$user = getenv('DB_USER') ?: '';
|
||||||
|
$pass = getenv('DB_PASSWORD') ?: '';
|
||||||
|
|
||||||
|
$dsn = "mysql:host={$host};port={$port};dbname={$dbName};charset=utf8mb4";
|
||||||
|
$pdo = new PDO($dsn, $user, $pass, [
|
||||||
|
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$sql = "SELECT TABLE_NAME, DEFINER, SECURITY_TYPE
|
||||||
|
FROM information_schema.VIEWS
|
||||||
|
WHERE TABLE_SCHEMA = DATABASE()
|
||||||
|
ORDER BY TABLE_NAME";
|
||||||
|
|
||||||
|
$rows = $pdo->query($sql)->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
if (!$rows) {
|
||||||
|
echo "No views found.\n";
|
||||||
|
exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($rows as $row) {
|
||||||
|
echo $row['TABLE_NAME'] . "\t" . ($row['DEFINER'] ?? '') . "\t" . ($row['SECURITY_TYPE'] ?? '') . "\n";
|
||||||
|
}
|
||||||
|
|
||||||
61
server/db/debug_reward_api_user123.php
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
define('BASE_PATH', dirname(__DIR__));
|
||||||
|
require BASE_PATH . '/vendor/autoload.php';
|
||||||
|
\Dotenv\Dotenv::createUnsafeMutable(BASE_PATH)->load();
|
||||||
|
\Webman\Config::load(BASE_PATH . '/config', ['route', 'plugin']);
|
||||||
|
\Webman\ThinkOrm\ThinkOrm::start(null);
|
||||||
|
|
||||||
|
use app\dice\helper\AdminScopeHelper;
|
||||||
|
use app\dice\logic\reward_config\DiceRewardConfigLogic;
|
||||||
|
use plugin\saiadmin\app\cache\UserInfoCache;
|
||||||
|
use support\think\Db;
|
||||||
|
|
||||||
|
$adminInfo = UserInfoCache::getUserInfo(123);
|
||||||
|
$logic = new DiceRewardConfigLogic();
|
||||||
|
$query = $logic->search([]);
|
||||||
|
AdminScopeHelper::applyConfigScope($query, $adminInfo, 0);
|
||||||
|
$_GET['limit'] = 200;
|
||||||
|
$_REQUEST['limit'] = 200;
|
||||||
|
$result = $logic->getList($query);
|
||||||
|
echo "limit=200 data count: " . count($result['data'] ?? []) . " total=" . ($result['total'] ?? 0) . "\n";
|
||||||
|
$bw = 0;
|
||||||
|
foreach ($result['data'] ?? [] as $row) {
|
||||||
|
if (($row['tier'] ?? '') === 'BIGWIN') {
|
||||||
|
$bw++;
|
||||||
|
echo "BIGWIN id={$row['id']} grid={$row['grid_number']}\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$query3 = $logic->search([]);
|
||||||
|
AdminScopeHelper::applyConfigScope($query3, $adminInfo, 0);
|
||||||
|
unset($_GET['limit'], $_REQUEST['limit']);
|
||||||
|
$result3 = $logic->getList($query3);
|
||||||
|
echo "default limit data count: " . count($result3['data'] ?? []) . "\n";
|
||||||
|
$bw3 = 0;
|
||||||
|
foreach ($result3['data'] ?? [] as $row) {
|
||||||
|
if (($row['tier'] ?? '') === 'BIGWIN') {
|
||||||
|
$bw3++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
echo "default limit BIGWIN: {$bw3}\n";
|
||||||
|
|
||||||
|
$_GET['saiType'] = 'all';
|
||||||
|
$_REQUEST['saiType'] = 'all';
|
||||||
|
$resultAll = $logic->getList($query);
|
||||||
|
echo "saiType=all count: " . (is_array($resultAll) ? count($resultAll) : 0) . "\n";
|
||||||
|
if (is_array($resultAll)) {
|
||||||
|
$bwAll = 0;
|
||||||
|
foreach ($resultAll as $row) {
|
||||||
|
if (($row['tier'] ?? '') === 'BIGWIN') {
|
||||||
|
$bwAll++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
echo "saiType=all BIGWIN: {$bwAll}\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "\nAll rows by id for dept 1123:\n";
|
||||||
|
$allRows = \support\think\Db::table('dice_reward_config')->where('dept_id', 1123)->order('id', 'asc')->select();
|
||||||
|
foreach ($allRows as $r) {
|
||||||
|
echo "id={$r['id']} tier={$r['tier']} grid={$r['grid_number']}\n";
|
||||||
|
}
|
||||||
46
server/db/debug_reward_index_user123.php
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
define('BASE_PATH', dirname(__DIR__));
|
||||||
|
require BASE_PATH . '/vendor/autoload.php';
|
||||||
|
\Dotenv\Dotenv::createUnsafeMutable(BASE_PATH)->load();
|
||||||
|
\Webman\Config::load(BASE_PATH . '/config', ['route', 'plugin']);
|
||||||
|
\Webman\ThinkOrm\ThinkOrm::start(null);
|
||||||
|
|
||||||
|
use app\dice\helper\AdminScopeHelper;
|
||||||
|
use app\dice\logic\reward_config\DiceRewardConfigLogic;
|
||||||
|
use plugin\saiadmin\app\cache\UserInfoCache;
|
||||||
|
|
||||||
|
$adminInfo = UserInfoCache::getUserInfo(123);
|
||||||
|
$logic = new DiceRewardConfigLogic();
|
||||||
|
|
||||||
|
// paginated default
|
||||||
|
$query = $logic->search([]);
|
||||||
|
AdminScopeHelper::applyConfigScope($query, $adminInfo, 0);
|
||||||
|
$page = $logic->getList($query);
|
||||||
|
echo "default paginate count=" . count($page['data'] ?? []) . " total=" . ($page['total'] ?? 0) . "\n";
|
||||||
|
|
||||||
|
// saiType all
|
||||||
|
$_GET['saiType'] = 'all';
|
||||||
|
$_REQUEST['saiType'] = 'all';
|
||||||
|
$query2 = $logic->search([]);
|
||||||
|
AdminScopeHelper::applyConfigScope($query2, $adminInfo, 0);
|
||||||
|
$all = $logic->getList($query2);
|
||||||
|
echo "saiType=all is_array=" . (is_array($all) ? 'yes' : 'no') . " count=" . (is_array($all) ? count($all) : 0) . "\n";
|
||||||
|
|
||||||
|
$nonBigwin = 0;
|
||||||
|
if (is_array($all)) {
|
||||||
|
foreach ($all as $row) {
|
||||||
|
if (($row['tier'] ?? '') !== 'BIGWIN') {
|
||||||
|
$nonBigwin++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
echo "non-BIGWIN rows for index tab: {$nonBigwin}\n";
|
||||||
|
|
||||||
|
// simulate dept_id=1123 explicit
|
||||||
|
unset($_GET['saiType'], $_REQUEST['saiType']);
|
||||||
|
$_GET['saiType'] = 'all';
|
||||||
|
$query3 = $logic->search([]);
|
||||||
|
AdminScopeHelper::applyConfigScope($query3, $adminInfo, 1123);
|
||||||
|
$all3 = $logic->getList($query3);
|
||||||
|
echo "dept_id=1123 saiType=all count=" . (is_array($all3) ? count($all3) : 0) . "\n";
|
||||||
39
server/db/debug_reward_query_detail.php
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
define('BASE_PATH', dirname(__DIR__));
|
||||||
|
require BASE_PATH . '/vendor/autoload.php';
|
||||||
|
\Dotenv\Dotenv::createUnsafeMutable(BASE_PATH)->load();
|
||||||
|
\Webman\Config::load(BASE_PATH . '/config', ['route', 'plugin']);
|
||||||
|
\Webman\ThinkOrm\ThinkOrm::start(null);
|
||||||
|
|
||||||
|
use app\dice\helper\AdminScopeHelper;
|
||||||
|
use app\dice\logic\reward_config\DiceRewardConfigLogic;
|
||||||
|
use app\dice\model\reward_config\DiceRewardConfig;
|
||||||
|
use plugin\saiadmin\app\cache\UserInfoCache;
|
||||||
|
use support\think\Db;
|
||||||
|
|
||||||
|
$adminInfo = UserInfoCache::getUserInfo(123);
|
||||||
|
|
||||||
|
echo "Raw DB count dept 1123: " . Db::table('dice_reward_config')->where('dept_id', 1123)->whereNull('delete_time')->count() . "\n";
|
||||||
|
|
||||||
|
$logic = new DiceRewardConfigLogic();
|
||||||
|
$query = $logic->search([]);
|
||||||
|
AdminScopeHelper::applyConfigScope($query, $adminInfo, 1123);
|
||||||
|
$sql = $query->fetchSql(true)->select();
|
||||||
|
echo "SQL: {$sql}\n";
|
||||||
|
$rows = $query->fetchSql(false)->select()->toArray();
|
||||||
|
echo "Model select count: " . count($rows) . "\n";
|
||||||
|
|
||||||
|
$model = new DiceRewardConfig();
|
||||||
|
$q2 = $model->where('dept_id', 1123)->order('id', 'asc');
|
||||||
|
$rows2 = $q2->select()->toArray();
|
||||||
|
echo "Direct model dept 1123: " . count($rows2) . "\n";
|
||||||
|
|
||||||
|
$tierCounts = Db::table('dice_reward_config')->where('dept_id', 1123)->group('tier')->column('count(*)', 'tier');
|
||||||
|
echo "Tier counts: " . json_encode($tierCounts, JSON_UNESCAPED_UNICODE) . "\n";
|
||||||
|
|
||||||
|
$idCounts = Db::query('SELECT id, COUNT(*) as c FROM dice_reward_config WHERE dept_id=1123 GROUP BY id HAVING c>1');
|
||||||
|
echo "Duplicate business ids in dept 1123: " . count($idCounts) . "\n";
|
||||||
|
if ($idCounts) {
|
||||||
|
print_r($idCounts);
|
||||||
|
}
|
||||||
36
server/db/debug_user123_reward.php
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
define('BASE_PATH', dirname(__DIR__));
|
||||||
|
require BASE_PATH . '/vendor/autoload.php';
|
||||||
|
\Dotenv\Dotenv::createUnsafeMutable(BASE_PATH)->load();
|
||||||
|
\Webman\Config::load(BASE_PATH . '/config', ['route', 'plugin']);
|
||||||
|
\Webman\ThinkOrm\ThinkOrm::start(null);
|
||||||
|
|
||||||
|
use app\dice\helper\AdminScopeHelper;
|
||||||
|
use plugin\saiadmin\app\cache\UserInfoCache;
|
||||||
|
use plugin\saiadmin\app\logic\system\SystemUserLogic;
|
||||||
|
use support\think\Db;
|
||||||
|
|
||||||
|
$uid = 123;
|
||||||
|
$user = Db::table('sa_system_user')->where('id', $uid)->find();
|
||||||
|
echo "DB user: " . json_encode($user, JSON_UNESCAPED_UNICODE) . "\n";
|
||||||
|
|
||||||
|
$logic = new SystemUserLogic();
|
||||||
|
$info = $logic->getUser($uid);
|
||||||
|
echo "getUser deptList: " . json_encode($info['deptList'] ?? null, JSON_UNESCAPED_UNICODE) . "\n";
|
||||||
|
echo "getUser dept_id: " . ($info['dept_id'] ?? 'null') . "\n";
|
||||||
|
|
||||||
|
$cached = UserInfoCache::getUserInfo($uid);
|
||||||
|
echo "cache deptList: " . json_encode($cached['deptList'] ?? null, JSON_UNESCAPED_UNICODE) . "\n";
|
||||||
|
echo "getDeptId: " . var_export(AdminScopeHelper::getDeptId($cached), true) . "\n";
|
||||||
|
echo "resolveConfigDeptId(null): " . AdminScopeHelper::resolveConfigDeptId($cached, null) . "\n";
|
||||||
|
echo "resolveConfigDeptId(0): " . AdminScopeHelper::resolveConfigDeptId($cached, 0) . "\n";
|
||||||
|
|
||||||
|
$deptId = 1123;
|
||||||
|
$all = Db::table('dice_reward_config')->where('dept_id', $deptId)->count();
|
||||||
|
$bigwin = Db::table('dice_reward_config')->where('dept_id', $deptId)->where('tier', 'BIGWIN')->count();
|
||||||
|
$reward = Db::table('dice_reward')->where('dept_id', $deptId)->count();
|
||||||
|
echo "dept {$deptId}: reward_config={$all}, BIGWIN={$bigwin}, dice_reward={$reward}\n";
|
||||||
|
|
||||||
|
$sample = Db::table('dice_reward_config')->where('dept_id', $deptId)->limit(3)->select();
|
||||||
|
echo "sample reward_config: " . json_encode($sample, JSON_UNESCAPED_UNICODE) . "\n";
|
||||||
26
server/db/dept_flatten_channels.sql
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
-- 渠道扁平化:将子渠道用户归并到顶级渠道,删除子渠道,更新表注释
|
||||||
|
-- 执行前请备份数据库
|
||||||
|
|
||||||
|
-- 1. 表及字段注释改为「渠道」
|
||||||
|
ALTER TABLE `sa_system_dept` COMMENT = '渠道表';
|
||||||
|
ALTER TABLE `sa_system_dept`
|
||||||
|
MODIFY COLUMN `parent_id` bigint(20) UNSIGNED NULL DEFAULT 0 COMMENT '父级ID(扁平渠道固定为0)',
|
||||||
|
MODIFY COLUMN `name` varchar(64) NOT NULL COMMENT '渠道名称',
|
||||||
|
MODIFY COLUMN `code` varchar(64) NULL DEFAULT NULL COMMENT '渠道编码',
|
||||||
|
MODIFY COLUMN `leader_id` bigint(20) UNSIGNED NULL DEFAULT NULL COMMENT '渠道负责人ID';
|
||||||
|
|
||||||
|
-- 2. 菜单名称(按实际 id 调整,id=5 为渠道管理菜单)
|
||||||
|
UPDATE `sa_system_menu` SET `name` = '渠道管理' WHERE `id` = 5 OR `name` LIKE '%渠道(部门)%' OR `name` = '部门管理';
|
||||||
|
|
||||||
|
-- 3. 将子渠道下的用户 dept_id 提升到顶级渠道(需配合 run_dept_flatten_channels.php 处理多级)
|
||||||
|
-- 以下为单级子渠道快速迁移(parent_id != 0 的直接挂到父级)
|
||||||
|
UPDATE `sa_system_user` u
|
||||||
|
INNER JOIN `sa_system_dept` d ON u.dept_id = d.id AND d.parent_id > 0
|
||||||
|
INNER JOIN `sa_system_dept` p ON d.parent_id = p.id
|
||||||
|
SET u.dept_id = p.id;
|
||||||
|
|
||||||
|
-- 4. 删除所有子渠道(parent_id > 0)
|
||||||
|
DELETE FROM `sa_system_dept` WHERE `parent_id` > 0;
|
||||||
|
|
||||||
|
-- 5. 剩余渠道统一为顶级
|
||||||
|
UPDATE `sa_system_dept` SET `parent_id` = 0, `level` = '0';
|
||||||
33
server/db/dice_flowcharts_menu.sql
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
-- 抽奖流程图:两个顶级外链菜单(type=4),点击新窗口打开 public/docs/flowcharts/*.html
|
||||||
|
-- 挂载位置:与「后台操作指南」同级(parent_id=0),紧挨其下方(sort 略小,列表按 sort 降序)
|
||||||
|
|
||||||
|
SET @now = NOW();
|
||||||
|
|
||||||
|
-- 移除旧的「抽奖流程说明」内嵌页菜单(若已安装)
|
||||||
|
SET @old_flow_menu_id = (
|
||||||
|
SELECT `id` FROM `sa_system_menu`
|
||||||
|
WHERE `path` = 'flowcharts' AND `component` = '/plugin/dice/flowcharts/index/index' AND `type` = 2
|
||||||
|
ORDER BY `id` ASC LIMIT 1
|
||||||
|
);
|
||||||
|
|
||||||
|
DELETE FROM `sa_system_role_menu` WHERE `menu_id` = @old_flow_menu_id;
|
||||||
|
DELETE FROM `sa_system_menu` WHERE `parent_id` = @old_flow_menu_id AND `type` = 3;
|
||||||
|
DELETE FROM `sa_system_menu` WHERE `id` = @old_flow_menu_id;
|
||||||
|
|
||||||
|
-- 1) 为何最终抽到该奖励
|
||||||
|
INSERT INTO `sa_system_menu`
|
||||||
|
(`parent_id`,`name`,`code`,`slug`,`type`,`path`,`component`,`method`,`icon`,`sort`,`link_url`,`is_iframe`,`is_keep_alive`,`is_hidden`,`is_fixed_tab`,`is_full_page`,`generate_id`,`generate_key`,`status`,`create_time`,`update_time`)
|
||||||
|
SELECT 0, '为何最终抽到该奖励', 'DiceFlowWhyReward', NULL, 4, 'dice_flow_why_reward', '', NULL, 'ri:question-answer-line', 4, '/docs/flowcharts/dice-为何抽到该奖励.html', 2, 2, 2, 2, 2, 0, NULL, 1, @now, @now
|
||||||
|
WHERE NOT EXISTS (
|
||||||
|
SELECT 1 FROM `sa_system_menu`
|
||||||
|
WHERE `type` = 4 AND `link_url` = '/docs/flowcharts/dice-为何抽到该奖励.html'
|
||||||
|
);
|
||||||
|
|
||||||
|
-- 2) 后台如何配置中奖逻辑
|
||||||
|
INSERT INTO `sa_system_menu`
|
||||||
|
(`parent_id`,`name`,`code`,`slug`,`type`,`path`,`component`,`method`,`icon`,`sort`,`link_url`,`is_iframe`,`is_keep_alive`,`is_hidden`,`is_fixed_tab`,`is_full_page`,`generate_id`,`generate_key`,`status`,`create_time`,`update_time`)
|
||||||
|
SELECT 0, '后台如何配置中奖逻辑', 'DiceFlowAdminConfig', NULL, 4, 'dice_flow_admin_config', '', NULL, 'ri:settings-3-line', 3, '/docs/flowcharts/dice-后台中奖逻辑配置.html', 2, 2, 2, 2, 2, 0, NULL, 1, @now, @now
|
||||||
|
WHERE NOT EXISTS (
|
||||||
|
SELECT 1 FROM `sa_system_menu`
|
||||||
|
WHERE `type` = 4 AND `link_url` = '/docs/flowcharts/dice-后台中奖逻辑配置.html'
|
||||||
|
);
|
||||||
3
server/db/dice_play_record_add_remark.sql
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
-- dice_play_record 新增备注(T4 惩罚余额不足等场景)
|
||||||
|
ALTER TABLE `dice_play_record`
|
||||||
|
ADD COLUMN `remark` varchar(255) DEFAULT NULL COMMENT '备注(如惩罚格余额不足)' AFTER `reward_tier`;
|
||||||
10
server/db/dice_player_dept_username_unique.sql
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
-- dice_player:同一渠道内用户名唯一(dept_id + username)— 数据库根本约束
|
||||||
|
-- 执行前请备份;若存在重复数据需先清理后再执行
|
||||||
|
-- 推荐:php db/run_dice_player_dept_username_unique.php
|
||||||
|
|
||||||
|
-- 移除仅按 username 的普通索引(若不存在可忽略报错)
|
||||||
|
-- ALTER TABLE `dice_player` DROP INDEX `idx_dice_player_username`;
|
||||||
|
|
||||||
|
-- 同一渠道下用户名唯一(UNIQUE 为数据库层最终约束)
|
||||||
|
ALTER TABLE `dice_player`
|
||||||
|
ADD UNIQUE INDEX `uk_dice_player_dept_username` (`dept_id`, `username`);
|
||||||
2
server/db/dice_player_username_index.sql
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
-- 已废弃:请使用 dice_player_dept_username_unique.sql(同一渠道 dept_id + username 唯一)
|
||||||
|
-- 历史脚本保留说明,勿再单独执行本文件
|
||||||
23
server/db/dice_reward_config_tier_recommend_menu.sql
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
-- 奖励配置:档位结算推荐配置(T1-T5 推荐金额、按规则生成)按钮权限
|
||||||
|
-- 挂载在「奖励配置」菜单(type=2)下;slug 与 DiceRewardConfigController Permission 一致
|
||||||
|
|
||||||
|
SET @now = NOW();
|
||||||
|
|
||||||
|
SET @reward_menu_id = (
|
||||||
|
SELECT `id` FROM `sa_system_menu`
|
||||||
|
WHERE `type` = 2
|
||||||
|
AND (
|
||||||
|
`path` = 'reward_config'
|
||||||
|
OR `component` LIKE '%reward_config%'
|
||||||
|
)
|
||||||
|
ORDER BY `id` ASC
|
||||||
|
LIMIT 1
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO `sa_system_menu`
|
||||||
|
(`parent_id`,`name`,`code`,`slug`,`type`,`path`,`component`,`method`,`sort`,`is_iframe`,`is_keep_alive`,`is_hidden`,`is_fixed_tab`,`is_full_page`,`generate_id`,`generate_key`,`status`,`create_time`,`update_time`)
|
||||||
|
SELECT @reward_menu_id, '档位结算推荐配置', '', 'dice:reward_config:index:tierRecommend', 3, '', '', '', 95, 2, 2, 2, 2, 2, 0, NULL, 1, @now, @now
|
||||||
|
WHERE @reward_menu_id IS NOT NULL
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1 FROM `sa_system_menu` WHERE `slug` = 'dice:reward_config:index:tierRecommend' AND `type` = 3
|
||||||
|
);
|
||||||
92
server/db/dice_tables_add_dept_id.sql
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
-- 大富翁游戏相关表增加 dept_id,关联 sa_system_dept(渠道表)
|
||||||
|
|
||||||
|
ALTER TABLE `dice_ante_config`
|
||||||
|
ADD COLUMN `dept_id` bigint(20) UNSIGNED NULL DEFAULT NULL COMMENT '所属渠道ID' AFTER `id`,
|
||||||
|
ADD INDEX `idx_dept_id` (`dept_id`);
|
||||||
|
|
||||||
|
ALTER TABLE `dice_config`
|
||||||
|
ADD COLUMN `dept_id` bigint(20) UNSIGNED NULL DEFAULT NULL COMMENT '所属渠道ID' AFTER `id`,
|
||||||
|
ADD INDEX `idx_dept_id` (`dept_id`);
|
||||||
|
|
||||||
|
ALTER TABLE `dice_game`
|
||||||
|
ADD COLUMN `dept_id` bigint(20) UNSIGNED NULL DEFAULT NULL COMMENT '所属渠道ID' AFTER `id`,
|
||||||
|
ADD INDEX `idx_dept_id` (`dept_id`);
|
||||||
|
|
||||||
|
ALTER TABLE `dice_lottery_config`
|
||||||
|
ADD COLUMN `dept_id` bigint(20) UNSIGNED NULL DEFAULT NULL COMMENT '所属渠道ID' AFTER `id`,
|
||||||
|
ADD INDEX `idx_dept_id` (`dept_id`);
|
||||||
|
|
||||||
|
ALTER TABLE `dice_lottery_poll_record`
|
||||||
|
ADD COLUMN `dept_id` bigint(20) UNSIGNED NULL DEFAULT NULL COMMENT '所属渠道ID' AFTER `id`,
|
||||||
|
ADD INDEX `idx_dept_id` (`dept_id`);
|
||||||
|
|
||||||
|
ALTER TABLE `dice_lottery_pool_config`
|
||||||
|
ADD COLUMN `dept_id` bigint(20) UNSIGNED NULL DEFAULT NULL COMMENT '所属渠道ID' AFTER `id`,
|
||||||
|
ADD INDEX `idx_dept_id` (`dept_id`);
|
||||||
|
|
||||||
|
ALTER TABLE `dice_play_record`
|
||||||
|
ADD COLUMN `dept_id` bigint(20) UNSIGNED NULL DEFAULT NULL COMMENT '所属渠道ID' AFTER `id`,
|
||||||
|
ADD INDEX `idx_dept_id` (`dept_id`);
|
||||||
|
|
||||||
|
ALTER TABLE `dice_play_record_test`
|
||||||
|
ADD COLUMN `dept_id` bigint(20) UNSIGNED NULL DEFAULT NULL COMMENT '所属渠道ID' AFTER `id`,
|
||||||
|
ADD INDEX `idx_dept_id` (`dept_id`);
|
||||||
|
|
||||||
|
ALTER TABLE `dice_player`
|
||||||
|
ADD COLUMN `dept_id` bigint(20) UNSIGNED NULL DEFAULT NULL COMMENT '所属渠道ID' AFTER `id`,
|
||||||
|
ADD INDEX `idx_dept_id` (`dept_id`);
|
||||||
|
|
||||||
|
-- 同一渠道内用户名唯一(根本约束,新库初始化时执行;已有库请用 dice_player_dept_username_unique.sql)
|
||||||
|
ALTER TABLE `dice_player`
|
||||||
|
ADD UNIQUE INDEX `uk_dice_player_dept_username` (`dept_id`, `username`);
|
||||||
|
|
||||||
|
ALTER TABLE `dice_player_ticket_record`
|
||||||
|
ADD COLUMN `dept_id` bigint(20) UNSIGNED NULL DEFAULT NULL COMMENT '所属渠道ID' AFTER `id`,
|
||||||
|
ADD INDEX `idx_dept_id` (`dept_id`);
|
||||||
|
|
||||||
|
ALTER TABLE `dice_player_wallet_record`
|
||||||
|
ADD COLUMN `dept_id` bigint(20) UNSIGNED NULL DEFAULT NULL COMMENT '所属渠道ID' AFTER `id`,
|
||||||
|
ADD INDEX `idx_dept_id` (`dept_id`);
|
||||||
|
|
||||||
|
ALTER TABLE `dice_reward`
|
||||||
|
ADD COLUMN `dept_id` bigint(20) UNSIGNED NULL DEFAULT NULL COMMENT '所属渠道ID' AFTER `id`,
|
||||||
|
ADD INDEX `idx_dept_id` (`dept_id`);
|
||||||
|
|
||||||
|
ALTER TABLE `dice_reward_config`
|
||||||
|
ADD COLUMN `dept_id` bigint(20) UNSIGNED NULL DEFAULT NULL COMMENT '所属渠道ID' AFTER `id`,
|
||||||
|
ADD INDEX `idx_dept_id` (`dept_id`);
|
||||||
|
|
||||||
|
ALTER TABLE `dice_reward_config_record`
|
||||||
|
ADD COLUMN `dept_id` bigint(20) UNSIGNED NULL DEFAULT NULL COMMENT '所属渠道ID' AFTER `id`,
|
||||||
|
ADD INDEX `idx_dept_id` (`dept_id`);
|
||||||
|
|
||||||
|
-- 从管理员归属回填玩家 dept_id
|
||||||
|
UPDATE `dice_player` p
|
||||||
|
INNER JOIN `sa_system_user` u ON p.admin_id = u.id
|
||||||
|
SET p.dept_id = u.dept_id
|
||||||
|
WHERE p.dept_id IS NULL AND u.dept_id IS NOT NULL AND u.dept_id > 0;
|
||||||
|
|
||||||
|
UPDATE `dice_play_record` r
|
||||||
|
INNER JOIN `dice_player` p ON r.player_id = p.id
|
||||||
|
SET r.dept_id = p.dept_id
|
||||||
|
WHERE r.dept_id IS NULL AND p.dept_id IS NOT NULL;
|
||||||
|
|
||||||
|
UPDATE `dice_play_record_test` r
|
||||||
|
INNER JOIN `dice_player` p ON r.player_id = p.id
|
||||||
|
SET r.dept_id = p.dept_id
|
||||||
|
WHERE r.dept_id IS NULL AND p.dept_id IS NOT NULL;
|
||||||
|
|
||||||
|
UPDATE `dice_player_ticket_record` r
|
||||||
|
INNER JOIN `dice_player` p ON r.player_id = p.id
|
||||||
|
SET r.dept_id = p.dept_id
|
||||||
|
WHERE r.dept_id IS NULL AND p.dept_id IS NOT NULL;
|
||||||
|
|
||||||
|
UPDATE `dice_player_wallet_record` r
|
||||||
|
INNER JOIN `dice_player` p ON r.player_id = p.id
|
||||||
|
SET r.dept_id = p.dept_id
|
||||||
|
WHERE r.dept_id IS NULL AND p.dept_id IS NOT NULL;
|
||||||
|
|
||||||
|
UPDATE `dice_reward_config_record` r
|
||||||
|
INNER JOIN `sa_system_user` u ON r.admin_id = u.id
|
||||||
|
SET r.dept_id = u.dept_id
|
||||||
|
WHERE r.dept_id IS NULL AND u.dept_id IS NOT NULL AND u.dept_id > 0;
|
||||||
127
server/db/fix_bt_backup_view_tables.php
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解决宝塔面板备份偶发报错:
|
||||||
|
* “备份文件中缺少表: xxx__view_backup”
|
||||||
|
*
|
||||||
|
* 原因通常是:数据库存在 VIEW,但宝塔的备份校验按“表”去匹配,
|
||||||
|
* 或其内部会期望存在 xxx__view_backup 之类的“视图备份表”标记。
|
||||||
|
*
|
||||||
|
* 本脚本会:
|
||||||
|
* - 扫描当前库所有 VIEW
|
||||||
|
* - 为每个 VIEW 创建一个同名的备份表:<view_name>__view_backup
|
||||||
|
* - 在该表写入 SHOW CREATE VIEW 的结果(若权限不足则写入空串)
|
||||||
|
*
|
||||||
|
* 用法(在 server 目录执行):
|
||||||
|
* php db/fix_bt_backup_view_tables.php
|
||||||
|
*/
|
||||||
|
|
||||||
|
require_once __DIR__ . '/../vendor/autoload.php';
|
||||||
|
|
||||||
|
if (class_exists(\Dotenv\Dotenv::class) && is_file(dirname(__DIR__) . '/.env')) {
|
||||||
|
if (method_exists(\Dotenv\Dotenv::class, 'createUnsafeMutable')) {
|
||||||
|
\Dotenv\Dotenv::createUnsafeMutable(dirname(__DIR__))->load();
|
||||||
|
} else {
|
||||||
|
\Dotenv\Dotenv::createMutable(dirname(__DIR__))->load();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$host = getenv('DB_HOST') ?: '127.0.0.1';
|
||||||
|
$port = getenv('DB_PORT') ?: '3306';
|
||||||
|
$dbName = getenv('DB_NAME') ?: '';
|
||||||
|
$user = getenv('DB_USER') ?: '';
|
||||||
|
$pass = getenv('DB_PASSWORD') ?: '';
|
||||||
|
|
||||||
|
if ($dbName === '' || $user === '') {
|
||||||
|
fwrite(STDERR, "Missing DB_NAME/DB_USER in .env\n");
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
$dsn = "mysql:host={$host};port={$port};dbname={$dbName};charset=utf8mb4";
|
||||||
|
$pdo = new PDO($dsn, $user, $pass, [
|
||||||
|
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$views = $pdo
|
||||||
|
->query("SELECT TABLE_NAME FROM information_schema.VIEWS WHERE TABLE_SCHEMA = DATABASE() ORDER BY TABLE_NAME")
|
||||||
|
->fetchAll(PDO::FETCH_COLUMN);
|
||||||
|
|
||||||
|
if (!$views) {
|
||||||
|
echo "No views found; nothing to do.\n";
|
||||||
|
exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
$created = 0;
|
||||||
|
$updated = 0;
|
||||||
|
$failed = 0;
|
||||||
|
|
||||||
|
foreach ($views as $viewName) {
|
||||||
|
$viewName = (string) $viewName;
|
||||||
|
$backupTable = $viewName . '__view_backup';
|
||||||
|
|
||||||
|
if (!preg_match('/^[a-zA-Z0-9_]+$/', $backupTable)) {
|
||||||
|
$failed++;
|
||||||
|
echo "[skip] invalid name: {$backupTable}\n";
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$pdo->exec(
|
||||||
|
"CREATE TABLE IF NOT EXISTS `{$backupTable}` (
|
||||||
|
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||||
|
`view_name` VARCHAR(255) NOT NULL,
|
||||||
|
`create_sql` LONGTEXT NOT NULL,
|
||||||
|
`updated_at` DATETIME NOT NULL,
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
UNIQUE KEY `uk_view_name` (`view_name`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4"
|
||||||
|
);
|
||||||
|
$created++;
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
$failed++;
|
||||||
|
echo "[fail] create table {$backupTable}: " . $e->getMessage() . "\n";
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$createSql = '';
|
||||||
|
try {
|
||||||
|
$stmt = $pdo->query("SHOW CREATE VIEW `{$viewName}`");
|
||||||
|
$row = $stmt ? $stmt->fetch(PDO::FETCH_ASSOC) : false;
|
||||||
|
if ($row) {
|
||||||
|
// SHOW CREATE VIEW 返回字段名可能是 "Create View" 或类似
|
||||||
|
foreach ($row as $k => $v) {
|
||||||
|
if (is_string($k) && stripos($k, 'create') !== false && is_string($v)) {
|
||||||
|
$createSql = $v;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
// 权限不足也不阻断,只是留空,保证备份表存在
|
||||||
|
$createSql = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$stmt = $pdo->prepare(
|
||||||
|
"INSERT INTO `{$backupTable}` (`view_name`, `create_sql`, `updated_at`)
|
||||||
|
VALUES (:view_name, :create_sql, :updated_at)
|
||||||
|
ON DUPLICATE KEY UPDATE
|
||||||
|
`create_sql` = VALUES(`create_sql`),
|
||||||
|
`updated_at` = VALUES(`updated_at`)"
|
||||||
|
);
|
||||||
|
$stmt->execute([
|
||||||
|
'view_name' => $viewName,
|
||||||
|
'create_sql' => $createSql,
|
||||||
|
'updated_at' => date('Y-m-d H:i:s'),
|
||||||
|
]);
|
||||||
|
$updated++;
|
||||||
|
echo "[ok] {$viewName} -> {$backupTable}\n";
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
$failed++;
|
||||||
|
echo "[fail] upsert {$backupTable}: " . $e->getMessage() . "\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "Done. created={$created}, updated={$updated}, failed={$failed}\n";
|
||||||
|
|
||||||
93
server/db/inspect_player_destroy.php
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* 排查 dice_player 删除报错:
|
||||||
|
* - 表结构
|
||||||
|
* - 外键引用情况
|
||||||
|
* - 实际 destroy 流程(不真的删,仅 dry-run 抓异常)
|
||||||
|
*
|
||||||
|
* 用法:php server/db/inspect_player_destroy.php [<id1>,<id2>,...]
|
||||||
|
*/
|
||||||
|
require_once __DIR__ . '/../vendor/autoload.php';
|
||||||
|
require_once __DIR__ . '/../support/bootstrap.php';
|
||||||
|
|
||||||
|
use support\think\Db;
|
||||||
|
|
||||||
|
$config = config('database');
|
||||||
|
$default = $config['default'];
|
||||||
|
$conn = $config['connections'][$default];
|
||||||
|
$dbName = $conn['database'] ?? '';
|
||||||
|
echo "[DB] {$default} -> {$dbName}\n\n";
|
||||||
|
|
||||||
|
echo "--- dice_player columns ---\n";
|
||||||
|
$cols = Db::query("SHOW FULL COLUMNS FROM `dice_player`");
|
||||||
|
foreach ($cols as $c) {
|
||||||
|
echo str_pad((string)$c['Field'], 26) . ' | ' . str_pad((string)$c['Type'], 24) . ' | NULL=' . $c['Null'] . ' | Key=' . $c['Key'] . "\n";
|
||||||
|
}
|
||||||
|
echo "\n";
|
||||||
|
|
||||||
|
echo "--- referenced by foreign keys (other tables -> dice_player) ---\n";
|
||||||
|
$fks = Db::query("SELECT TABLE_NAME, COLUMN_NAME, CONSTRAINT_NAME, REFERENCED_TABLE_NAME, REFERENCED_COLUMN_NAME
|
||||||
|
FROM information_schema.KEY_COLUMN_USAGE
|
||||||
|
WHERE TABLE_SCHEMA = ? AND REFERENCED_TABLE_NAME = 'dice_player'", [$dbName]);
|
||||||
|
if (empty($fks)) {
|
||||||
|
echo " (none)\n";
|
||||||
|
} else {
|
||||||
|
foreach ($fks as $f) {
|
||||||
|
echo " {$f['TABLE_NAME']}.{$f['COLUMN_NAME']} -> {$f['REFERENCED_TABLE_NAME']}.{$f['REFERENCED_COLUMN_NAME']} [{$f['CONSTRAINT_NAME']}]\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
echo "\n";
|
||||||
|
|
||||||
|
echo "--- top 3 dice_player rows ---\n";
|
||||||
|
$rows = Db::table('dice_player')->limit(3)->select()->toArray();
|
||||||
|
foreach ($rows as $r) {
|
||||||
|
echo "id={$r['id']} dept_id={$r['dept_id']} username={$r['username']} delete_time=" . ($r['delete_time'] ?? 'null') . "\n";
|
||||||
|
}
|
||||||
|
echo "\n";
|
||||||
|
|
||||||
|
$idsArg = $argv[1] ?? '';
|
||||||
|
if ($idsArg === '') {
|
||||||
|
echo "(no ids passed, skip dry-run delete)\n";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$ids = array_filter(array_map('intval', explode(',', $idsArg)));
|
||||||
|
if (empty($ids)) {
|
||||||
|
echo "(invalid ids)\n";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "--- dry-run destroy ids: " . implode(',', $ids) . " ---\n";
|
||||||
|
$beforeAll = Db::query('SELECT id, delete_time FROM dice_player WHERE id IN (' . implode(',', array_fill(0, count($ids), '?')) . ')', $ids);
|
||||||
|
echo "before (raw): " . count($beforeAll) . " rows\n";
|
||||||
|
|
||||||
|
$placeholders = implode(',', array_fill(0, count($ids), '?'));
|
||||||
|
|
||||||
|
// Case A: static destroy
|
||||||
|
try {
|
||||||
|
Db::startTrans();
|
||||||
|
$result = \app\dice\model\player\DicePlayer::destroy($ids, true);
|
||||||
|
$afterAny = Db::query("SELECT COUNT(*) AS c FROM dice_player WHERE id IN ({$placeholders})", $ids);
|
||||||
|
echo "[static destroy] returned: " . var_export($result, true) . ", remaining raw rows: " . ($afterAny[0]['c'] ?? 'n/a') . "\n";
|
||||||
|
Db::rollback();
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
Db::rollback();
|
||||||
|
echo "EXCEPTION (static destroy): " . get_class($e) . ": " . $e->getMessage() . "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Case B: instance ->delete()
|
||||||
|
try {
|
||||||
|
Db::startTrans();
|
||||||
|
$instance = \app\dice\model\player\DicePlayer::find($ids[0]);
|
||||||
|
if ($instance) {
|
||||||
|
$r = $instance->delete();
|
||||||
|
$afterAny = Db::query("SELECT COUNT(*) AS c FROM dice_player WHERE id = ?", [$ids[0]]);
|
||||||
|
echo "[instance delete] returned: " . var_export($r, true) . ", remaining raw rows for id={$ids[0]}: " . ($afterAny[0]['c'] ?? 'n/a') . "\n";
|
||||||
|
} else {
|
||||||
|
echo "[instance delete] no instance found for id={$ids[0]}\n";
|
||||||
|
}
|
||||||
|
Db::rollback();
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
Db::rollback();
|
||||||
|
echo "EXCEPTION (instance delete): " . get_class($e) . ": " . $e->getMessage() . "\n";
|
||||||
|
}
|
||||||
|
echo "(both rolled back, no actual delete)\n";
|
||||||
35
server/db/remove_safeguard_ops_role_menus.sql
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
-- 从所有角色中移除以下运维菜单及其按钮权限
|
||||||
|
-- /safeguard/dict
|
||||||
|
-- /safeguard/attachment
|
||||||
|
-- /safeguard/database
|
||||||
|
-- /safeguard/server
|
||||||
|
-- /safeguard/cache
|
||||||
|
-- /safeguard/email-log
|
||||||
|
--
|
||||||
|
-- 推荐执行:php db/run_remove_safeguard_ops_role_menus.php
|
||||||
|
-- 该脚本会按 route 动态匹配菜单及其子权限,并清理菜单缓存
|
||||||
|
|
||||||
|
-- 主菜单
|
||||||
|
DELETE rm FROM `sa_system_role_menu` rm
|
||||||
|
INNER JOIN `sa_system_menu` m ON rm.menu_id = m.id
|
||||||
|
WHERE m.component IN (
|
||||||
|
'/safeguard/dict',
|
||||||
|
'/safeguard/attachment',
|
||||||
|
'/safeguard/database',
|
||||||
|
'/safeguard/server',
|
||||||
|
'/safeguard/cache',
|
||||||
|
'/safeguard/email-log'
|
||||||
|
);
|
||||||
|
|
||||||
|
-- 子按钮权限
|
||||||
|
DELETE rm FROM `sa_system_role_menu` rm
|
||||||
|
INNER JOIN `sa_system_menu` child ON rm.menu_id = child.id
|
||||||
|
INNER JOIN `sa_system_menu` parent ON child.parent_id = parent.id
|
||||||
|
WHERE parent.component IN (
|
||||||
|
'/safeguard/dict',
|
||||||
|
'/safeguard/attachment',
|
||||||
|
'/safeguard/database',
|
||||||
|
'/safeguard/server',
|
||||||
|
'/safeguard/cache',
|
||||||
|
'/safeguard/email-log'
|
||||||
|
);
|
||||||
14
server/db/remove_system_post.sql
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
-- 移除岗位(Post)相关数据表与菜单
|
||||||
|
-- 执行前请备份数据库
|
||||||
|
|
||||||
|
-- 删除岗位子菜单权限(id: 41-47)
|
||||||
|
DELETE FROM `sa_system_role_menu` WHERE `menu_id` IN (41, 42, 43, 44, 45, 46, 47);
|
||||||
|
DELETE FROM `sa_system_menu` WHERE `id` IN (41, 42, 43, 44, 45, 46, 47);
|
||||||
|
|
||||||
|
-- 删除岗位管理主菜单(id: 7)
|
||||||
|
DELETE FROM `sa_system_role_menu` WHERE `menu_id` = 7;
|
||||||
|
DELETE FROM `sa_system_menu` WHERE `id` = 7;
|
||||||
|
|
||||||
|
-- 删除用户岗位关联表与岗位信息表
|
||||||
|
DROP TABLE IF EXISTS `sa_system_user_post`;
|
||||||
|
DROP TABLE IF EXISTS `sa_system_post`;
|
||||||
191
server/db/run_all_init.php
Normal file
@@ -0,0 +1,191 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* 一键执行渠道与游戏配置初始化(需已备份数据库)
|
||||||
|
* 用法:在 server 目录执行 php db/run_all_init.php
|
||||||
|
*/
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
define('BASE_PATH', dirname(__DIR__));
|
||||||
|
|
||||||
|
require_once BASE_PATH . '/vendor/autoload.php';
|
||||||
|
|
||||||
|
if (class_exists(\Dotenv\Dotenv::class) && is_file(BASE_PATH . '/.env')) {
|
||||||
|
if (method_exists(\Dotenv\Dotenv::class, 'createUnsafeMutable')) {
|
||||||
|
\Dotenv\Dotenv::createUnsafeMutable(BASE_PATH)->load();
|
||||||
|
} else {
|
||||||
|
\Dotenv\Dotenv::createMutable(BASE_PATH)->load();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载配置(排除 route.php,避免 CLI 报错)
|
||||||
|
\Webman\Config::load(BASE_PATH . '/config', ['route', 'plugin']);
|
||||||
|
\Webman\ThinkOrm\ThinkOrm::start(null);
|
||||||
|
|
||||||
|
use app\dice\service\DiceChannelConfigService;
|
||||||
|
use plugin\saiadmin\app\model\system\SystemDept;
|
||||||
|
use plugin\saiadmin\app\model\system\SystemUser;
|
||||||
|
use support\think\Db;
|
||||||
|
|
||||||
|
function cliPdo(): PDO
|
||||||
|
{
|
||||||
|
$host = getenv('DB_HOST') ?: '127.0.0.1';
|
||||||
|
$port = getenv('DB_PORT') ?: '3306';
|
||||||
|
$db = getenv('DB_NAME') ?: '';
|
||||||
|
$user = getenv('DB_USER') ?: '';
|
||||||
|
$pass = getenv('DB_PASSWORD') ?: '';
|
||||||
|
$dsn = "mysql:host={$host};port={$port};dbname={$db};charset=utf8mb4";
|
||||||
|
return new PDO($dsn, $user, $pass, [
|
||||||
|
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function runSqlFile(PDO $pdo, string $path, string $label, bool $alterOnly = false): void
|
||||||
|
{
|
||||||
|
echo "\n=== {$label} ===\n";
|
||||||
|
if (!is_file($path)) {
|
||||||
|
echo "跳过:文件不存在\n";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$sql = file_get_contents($path);
|
||||||
|
$sql = preg_replace('/--.*$/m', '', $sql);
|
||||||
|
$parts = array_filter(array_map('trim', explode(';', $sql)));
|
||||||
|
$ok = 0;
|
||||||
|
$skip = 0;
|
||||||
|
foreach ($parts as $statement) {
|
||||||
|
if ($statement === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if ($alterOnly && stripos($statement, 'ALTER TABLE') !== 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
$pdo->exec($statement);
|
||||||
|
$ok++;
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
$msg = $e->getMessage();
|
||||||
|
$isAlter = stripos($statement, 'ALTER TABLE') === 0;
|
||||||
|
if ($isAlter && (stripos($msg, 'Duplicate column') !== false
|
||||||
|
|| stripos($msg, 'Duplicate key name') !== false)) {
|
||||||
|
$skip++;
|
||||||
|
} elseif (!$isAlter) {
|
||||||
|
echo " [警告] " . substr($msg, 0, 120) . "\n";
|
||||||
|
$skip++;
|
||||||
|
} else {
|
||||||
|
echo " [错误] {$msg}\n";
|
||||||
|
throw $e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
echo "完成:成功 {$ok} 条" . ($skip > 0 ? ",跳过 {$skip} 条(字段已存在)" : '') . "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRootDeptId(int $deptId): ?int
|
||||||
|
{
|
||||||
|
$currentId = $deptId;
|
||||||
|
$visited = [];
|
||||||
|
while ($currentId > 0 && !isset($visited[$currentId])) {
|
||||||
|
$visited[$currentId] = true;
|
||||||
|
$dept = SystemDept::find($currentId);
|
||||||
|
if (!$dept) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
$parentId = (int) ($dept->parent_id ?? 0);
|
||||||
|
if ($parentId === 0) {
|
||||||
|
return $currentId;
|
||||||
|
}
|
||||||
|
$currentId = $parentId;
|
||||||
|
}
|
||||||
|
return $currentId > 0 ? $currentId : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "========== 大富翁渠道/配置初始化 ==========\n";
|
||||||
|
echo '数据库: ' . (getenv('DB_NAME') ?: '') . '@' . (getenv('DB_HOST') ?: '') . "\n";
|
||||||
|
|
||||||
|
$pdo = cliPdo();
|
||||||
|
|
||||||
|
runSqlFile($pdo, __DIR__ . '/dice_tables_add_dept_id.sql', '1. dice 表增加 dept_id', true);
|
||||||
|
runSqlFile($pdo, __DIR__ . '/dept_flatten_channels.sql', '2. 渠道扁平化 SQL');
|
||||||
|
|
||||||
|
echo "\n=== 3. 渠道扁平化 PHP(多级用户归并 + 删除子渠道) ===\n";
|
||||||
|
Db::transaction(function () {
|
||||||
|
$users = SystemUser::where('dept_id', '>', 0)->select();
|
||||||
|
$moved = 0;
|
||||||
|
foreach ($users as $user) {
|
||||||
|
$deptId = (int) $user->dept_id;
|
||||||
|
$rootId = getRootDeptId($deptId);
|
||||||
|
if ($rootId !== null && $rootId !== $deptId) {
|
||||||
|
SystemUser::where('id', $user->id)->update(['dept_id' => $rootId]);
|
||||||
|
echo " 用户 {$user->id}: dept {$deptId} -> {$rootId}\n";
|
||||||
|
$moved++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$childIds = SystemDept::where('parent_id', '>', 0)->column('id');
|
||||||
|
if (!empty($childIds)) {
|
||||||
|
SystemDept::destroy($childIds);
|
||||||
|
echo ' 已删除子渠道: ' . implode(',', $childIds) . "\n";
|
||||||
|
} else {
|
||||||
|
echo " 无子渠道需删除\n";
|
||||||
|
}
|
||||||
|
SystemDept::where('id', '>', 0)->update(['parent_id' => 0, 'level' => '0']);
|
||||||
|
echo " 用户归并 {$moved} 人,渠道扁平化完成\n";
|
||||||
|
});
|
||||||
|
|
||||||
|
$service = new DiceChannelConfigService();
|
||||||
|
|
||||||
|
echo "\n=== 3.5 配置表复合键与默认模板 dept_id=0 ===\n";
|
||||||
|
$service->ensureConfigCompositeKeys();
|
||||||
|
echo " dice_config / dice_reward_config: ok\n";
|
||||||
|
|
||||||
|
echo "\n=== 3.6 彩金池配置按渠道唯一(dept_id + name) ===\n";
|
||||||
|
try {
|
||||||
|
$indexes = Db::query("SHOW INDEX FROM `dice_lottery_pool_config` WHERE Key_name = 'dice_lottery_poll_config_unique'");
|
||||||
|
if (!empty($indexes)) {
|
||||||
|
Db::execute('ALTER TABLE `dice_lottery_pool_config` DROP INDEX `dice_lottery_poll_config_unique`');
|
||||||
|
echo " 已移除 name 全局唯一索引\n";
|
||||||
|
}
|
||||||
|
$uk = Db::query("SHOW INDEX FROM `dice_lottery_pool_config` WHERE Key_name = 'uk_dept_name'");
|
||||||
|
if (empty($uk)) {
|
||||||
|
Db::execute('ALTER TABLE `dice_lottery_pool_config` ADD UNIQUE KEY `uk_dept_name` (`dept_id`, `name`)');
|
||||||
|
echo " 已添加 uk_dept_name(dept_id, name)\n";
|
||||||
|
} else {
|
||||||
|
echo " uk_dept_name 已存在\n";
|
||||||
|
}
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
echo " 跳过: {$e->getMessage()}\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "\n=== 4. 将现有配置设为默认模板(dept_id=0) ===\n";
|
||||||
|
$tables = [
|
||||||
|
'dice_config',
|
||||||
|
'dice_ante_config',
|
||||||
|
'dice_lottery_pool_config',
|
||||||
|
'dice_reward_config',
|
||||||
|
'dice_reward',
|
||||||
|
'dice_game',
|
||||||
|
];
|
||||||
|
foreach ($tables as $table) {
|
||||||
|
try {
|
||||||
|
Db::table($table)->update(['dept_id' => 0]);
|
||||||
|
echo " {$table}: ok\n";
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
echo " {$table}: 跳过 ({$e->getMessage()})\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "\n=== 5. 为所有渠道从默认模板复制配置 ===\n";
|
||||||
|
$summary = $service->syncAllChannelsFromDefault();
|
||||||
|
foreach ($summary as $deptId => $info) {
|
||||||
|
$copied = implode(',', $info['copied'] ?? []) ?: '无';
|
||||||
|
$skipped = implode(',', $info['skipped'] ?? []) ?: '无';
|
||||||
|
echo " 渠道 {$deptId}: 复制 [{$copied}],跳过 [{$skipped}]\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "\n=== 6. 修复无效渠道并回填 dept_id ===\n";
|
||||||
|
echo " 无效渠道归并: ";
|
||||||
|
print_r($service->repairOrphanDeptReferences());
|
||||||
|
echo " 回填: ";
|
||||||
|
print_r($service->backfillDataDeptId());
|
||||||
|
|
||||||
|
$deptCount = SystemDept::count();
|
||||||
|
echo "\n当前顶级渠道数: {$deptCount}\n";
|
||||||
|
echo "========== 全部初始化完成 ==========\n";
|
||||||
16
server/db/run_backfill_dept_id.php
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
define('BASE_PATH', dirname(__DIR__));
|
||||||
|
require_once BASE_PATH . '/vendor/autoload.php';
|
||||||
|
\Dotenv\Dotenv::createUnsafeMutable(BASE_PATH)->load();
|
||||||
|
\Webman\Config::load(BASE_PATH . '/config', ['route', 'plugin']);
|
||||||
|
\Webman\ThinkOrm\ThinkOrm::start(null);
|
||||||
|
|
||||||
|
use app\dice\service\DiceChannelConfigService;
|
||||||
|
|
||||||
|
$service = new DiceChannelConfigService();
|
||||||
|
echo "修复无效渠道引用...\n";
|
||||||
|
print_r($service->repairOrphanDeptReferences());
|
||||||
|
echo "\n回填 dept_id...\n";
|
||||||
|
print_r($service->backfillDataDeptId());
|
||||||
|
echo "完成。\n";
|
||||||
46
server/db/run_channel_config_init.php
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* 渠道配置初始化:默认模板 + 为已有渠道补齐配置 + 业务数据 dept_id 回填
|
||||||
|
* 用法:在 server 目录执行 php db/run_channel_config_init.php
|
||||||
|
*/
|
||||||
|
require_once __DIR__ . '/../vendor/autoload.php';
|
||||||
|
|
||||||
|
$bootstrap = __DIR__ . '/../support/bootstrap.php';
|
||||||
|
if (is_file($bootstrap)) {
|
||||||
|
require_once $bootstrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
use app\dice\service\DiceChannelConfigService;
|
||||||
|
use support\think\Db;
|
||||||
|
|
||||||
|
$service = new DiceChannelConfigService();
|
||||||
|
|
||||||
|
echo "1. 将 dept_id=0 的配置归为默认模板(dept_id 置空)...\n";
|
||||||
|
echo " 若需将全部现有配置作为模板,请确认后手动执行 UPDATE ... SET dept_id=NULL\n";
|
||||||
|
foreach (
|
||||||
|
[
|
||||||
|
'dice_config',
|
||||||
|
'dice_ante_config',
|
||||||
|
'dice_lottery_pool_config',
|
||||||
|
'dice_reward_config',
|
||||||
|
'dice_reward',
|
||||||
|
'dice_game',
|
||||||
|
] as $table
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
Db::table($table)->where('dept_id', 0)->update(['dept_id' => null]);
|
||||||
|
echo " {$table}: ok\n";
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
echo " {$table}: 跳过 ({$e->getMessage()})\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "2. 为所有渠道补齐默认配置...\n";
|
||||||
|
$summary = $service->syncAllChannelsFromDefault();
|
||||||
|
print_r($summary);
|
||||||
|
|
||||||
|
echo "3. 回填玩家及记录 dept_id...\n";
|
||||||
|
$stats = $service->backfillDataDeptId();
|
||||||
|
print_r($stats);
|
||||||
|
|
||||||
|
echo "完成。\n";
|
||||||
55
server/db/run_dept_flatten_channels.php
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* 渠道扁平化迁移脚本(多级子渠道用户归并到顶级渠道后删除子渠道)
|
||||||
|
* 用法:在 server 目录下执行 php db/run_dept_flatten_channels.php
|
||||||
|
*/
|
||||||
|
require_once __DIR__ . '/../vendor/autoload.php';
|
||||||
|
|
||||||
|
use plugin\saiadmin\app\model\system\SystemDept;
|
||||||
|
use plugin\saiadmin\app\model\system\SystemUser;
|
||||||
|
use support\think\Db;
|
||||||
|
|
||||||
|
$bootstrap = __DIR__ . '/../support/bootstrap.php';
|
||||||
|
if (is_file($bootstrap)) {
|
||||||
|
require_once $bootstrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRootDeptId(int $deptId): ?int
|
||||||
|
{
|
||||||
|
$currentId = $deptId;
|
||||||
|
$visited = [];
|
||||||
|
while ($currentId > 0 && !isset($visited[$currentId])) {
|
||||||
|
$visited[$currentId] = true;
|
||||||
|
$dept = SystemDept::find($currentId);
|
||||||
|
if (!$dept) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
$parentId = (int) ($dept->parent_id ?? 0);
|
||||||
|
if ($parentId === 0) {
|
||||||
|
return $currentId;
|
||||||
|
}
|
||||||
|
$currentId = $parentId;
|
||||||
|
}
|
||||||
|
return $currentId > 0 ? $currentId : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Db::transaction(function () {
|
||||||
|
$users = SystemUser::where('dept_id', '>', 0)->select();
|
||||||
|
foreach ($users as $user) {
|
||||||
|
$deptId = (int) $user->dept_id;
|
||||||
|
$rootId = getRootDeptId($deptId);
|
||||||
|
if ($rootId !== null && $rootId !== $deptId) {
|
||||||
|
SystemUser::where('id', $user->id)->update(['dept_id' => $rootId]);
|
||||||
|
echo "用户 {$user->id} dept_id {$deptId} -> {$rootId}\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$childIds = SystemDept::where('parent_id', '>', 0)->column('id');
|
||||||
|
if (!empty($childIds)) {
|
||||||
|
SystemDept::destroy($childIds);
|
||||||
|
echo '已删除子渠道: ' . implode(',', $childIds) . "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
SystemDept::where('id', '>', 0)->update(['parent_id' => 0, 'level' => '0']);
|
||||||
|
echo "渠道扁平化完成\n";
|
||||||
|
});
|
||||||
99
server/db/run_dice_flowcharts_menu.php
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* 安装两个抽奖流程图外链菜单,并授权超级管理员
|
||||||
|
* 用法(在 server 目录): php db/run_dice_flowcharts_menu.php
|
||||||
|
*/
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
require_once __DIR__ . '/../vendor/autoload.php';
|
||||||
|
require_once __DIR__ . '/../support/bootstrap.php';
|
||||||
|
|
||||||
|
use plugin\saiadmin\app\cache\UserMenuCache;
|
||||||
|
use support\think\Db;
|
||||||
|
|
||||||
|
function runSqlFile(PDO $pdo, string $path, string $label): void
|
||||||
|
{
|
||||||
|
echo "\n=== {$label} ===\n";
|
||||||
|
if (! is_file($path)) {
|
||||||
|
echo "跳过:文件不存在 {$path}\n";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$sql = file_get_contents($path);
|
||||||
|
$sql = preg_replace('/--.*$/m', '', $sql);
|
||||||
|
$parts = array_filter(array_map('trim', explode(';', $sql)));
|
||||||
|
$ok = 0;
|
||||||
|
foreach ($parts as $statement) {
|
||||||
|
if ($statement === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$pdo->exec($statement);
|
||||||
|
$ok++;
|
||||||
|
}
|
||||||
|
echo "完成:执行 {$ok} 条语句\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
function cliPdo(): PDO
|
||||||
|
{
|
||||||
|
$host = getenv('DB_HOST') ?: '127.0.0.1';
|
||||||
|
$port = getenv('DB_PORT') ?: '3306';
|
||||||
|
$db = getenv('DB_NAME') ?: '';
|
||||||
|
$user = getenv('DB_USER') ?: '';
|
||||||
|
$pass = getenv('DB_PASSWORD') ?: '';
|
||||||
|
$dsn = "mysql:host={$host};port={$port};dbname={$db};charset=utf8mb4";
|
||||||
|
return new PDO($dsn, $user, $pass, [
|
||||||
|
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "========== 抽奖流程图外链菜单安装 ==========\n";
|
||||||
|
echo '数据库: ' . (getenv('DB_NAME') ?: '') . '@' . (getenv('DB_HOST') ?: '') . "\n";
|
||||||
|
|
||||||
|
$pdo = cliPdo();
|
||||||
|
runSqlFile($pdo, __DIR__ . '/dice_flowcharts_menu.sql', '1. 外链菜单');
|
||||||
|
|
||||||
|
$menuIds = Db::name('sa_system_menu')
|
||||||
|
->where('type', 4)
|
||||||
|
->whereIn('link_url', [
|
||||||
|
'/docs/flowcharts/dice-为何抽到该奖励.html',
|
||||||
|
'/docs/flowcharts/dice-后台中奖逻辑配置.html',
|
||||||
|
])
|
||||||
|
->column('id');
|
||||||
|
|
||||||
|
if ($menuIds === [] || $menuIds === null) {
|
||||||
|
echo "错误:未找到流程图菜单\n";
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
$menuIds = array_map('intval', $menuIds);
|
||||||
|
echo "\n流程图菜单 ID: " . implode(', ', $menuIds) . "\n";
|
||||||
|
|
||||||
|
echo "\n=== 2. 授权超级管理员角色 ===\n";
|
||||||
|
$adminRoleIds = Db::name('sa_system_role')
|
||||||
|
->where('code', 'super_admin')
|
||||||
|
->column('id');
|
||||||
|
|
||||||
|
if ($adminRoleIds === [] || $adminRoleIds === null) {
|
||||||
|
$adminRoleIds = Db::name('sa_system_role')->where('id', 1)->column('id');
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($adminRoleIds as $roleId) {
|
||||||
|
$roleId = (int) $roleId;
|
||||||
|
foreach ($menuIds as $menuId) {
|
||||||
|
$exists = Db::name('sa_system_role_menu')
|
||||||
|
->where('role_id', $roleId)
|
||||||
|
->where('menu_id', $menuId)
|
||||||
|
->count();
|
||||||
|
if ($exists > 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Db::name('sa_system_role_menu')->insert([
|
||||||
|
'role_id' => $roleId,
|
||||||
|
'menu_id' => $menuId,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
echo "角色 {$roleId} 已关联流程图菜单\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
UserMenuCache::clearMenuCache();
|
||||||
|
echo "\n已清理菜单缓存。请重新登录后台或刷新页面查看侧边栏。\n";
|
||||||
|
echo "点击菜单将在新窗口打开 /docs/flowcharts/*.html\n";
|
||||||
44
server/db/run_dice_play_record_add_remark.php
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* 执行 dice_play_record 备注字段迁移
|
||||||
|
* 用法:在 server 目录执行 php db/run_dice_play_record_add_remark.php
|
||||||
|
*/
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
define('BASE_PATH', dirname(__DIR__));
|
||||||
|
|
||||||
|
require_once BASE_PATH . '/vendor/autoload.php';
|
||||||
|
|
||||||
|
if (class_exists(\Dotenv\Dotenv::class) && is_file(BASE_PATH . '/.env')) {
|
||||||
|
if (method_exists(\Dotenv\Dotenv::class, 'createUnsafeMutable')) {
|
||||||
|
\Dotenv\Dotenv::createUnsafeMutable(BASE_PATH)->load();
|
||||||
|
} else {
|
||||||
|
\Dotenv\Dotenv::createMutable(BASE_PATH)->load();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$host = getenv('DB_HOST') ?: '127.0.0.1';
|
||||||
|
$port = getenv('DB_PORT') ?: '3306';
|
||||||
|
$db = getenv('DB_NAME') ?: '';
|
||||||
|
$user = getenv('DB_USER') ?: '';
|
||||||
|
$pass = getenv('DB_PASSWORD') ?: '';
|
||||||
|
$dsn = "mysql:host={$host};port={$port};dbname={$db};charset=utf8mb4";
|
||||||
|
$pdo = new PDO($dsn, $user, $pass, [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);
|
||||||
|
|
||||||
|
$sqlFile = __DIR__ . '/dice_play_record_add_remark.sql';
|
||||||
|
$sql = file_get_contents($sqlFile);
|
||||||
|
$sql = preg_replace('/--.*$/m', '', $sql);
|
||||||
|
$parts = array_filter(array_map('trim', explode(';', $sql)));
|
||||||
|
|
||||||
|
echo "执行: dice_play_record_add_remark.sql\n";
|
||||||
|
echo "数据库: {$db} @ {$host}\n\n";
|
||||||
|
|
||||||
|
foreach ($parts as $statement) {
|
||||||
|
if ($statement === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$pdo->exec($statement);
|
||||||
|
echo "OK\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "\n完成。\n";
|
||||||
53
server/db/run_dice_player_dept_username_unique.php
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* 为 dice_player 增加 (dept_id, username) 唯一索引
|
||||||
|
* 用法:在 server 目录执行 php db/run_dice_player_dept_username_unique.php
|
||||||
|
*/
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
define('BASE_PATH', dirname(__DIR__));
|
||||||
|
|
||||||
|
require_once BASE_PATH . '/vendor/autoload.php';
|
||||||
|
|
||||||
|
if (class_exists(\Dotenv\Dotenv::class) && is_file(BASE_PATH . '/.env')) {
|
||||||
|
if (method_exists(\Dotenv\Dotenv::class, 'createUnsafeMutable')) {
|
||||||
|
\Dotenv\Dotenv::createUnsafeMutable(BASE_PATH)->load();
|
||||||
|
} else {
|
||||||
|
\Dotenv\Dotenv::createMutable(BASE_PATH)->load();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
\Webman\Config::load(BASE_PATH . '/config', ['route', 'plugin']);
|
||||||
|
\Webman\ThinkOrm\ThinkOrm::start(null);
|
||||||
|
|
||||||
|
use support\think\Db;
|
||||||
|
|
||||||
|
echo "检查 (dept_id, username) 重复...\n";
|
||||||
|
$dupes = Db::query(
|
||||||
|
'SELECT dept_id, username, COUNT(*) AS c FROM dice_player
|
||||||
|
WHERE username IS NOT NULL AND username <> \'\'
|
||||||
|
GROUP BY dept_id, username HAVING c > 1 LIMIT 20'
|
||||||
|
);
|
||||||
|
if (!empty($dupes)) {
|
||||||
|
echo "存在重复记录,请先处理后再执行:\n";
|
||||||
|
print_r($dupes);
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
$indexes = Db::query("SHOW INDEX FROM `dice_player` WHERE Key_name = 'idx_dice_player_username'");
|
||||||
|
if (!empty($indexes)) {
|
||||||
|
Db::execute('ALTER TABLE `dice_player` DROP INDEX `idx_dice_player_username`');
|
||||||
|
echo "已删除 idx_dice_player_username\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
$uk = Db::query("SHOW INDEX FROM `dice_player` WHERE Key_name = 'uk_dice_player_dept_username'");
|
||||||
|
if (empty($uk)) {
|
||||||
|
Db::execute(
|
||||||
|
'ALTER TABLE `dice_player` ADD UNIQUE INDEX `uk_dice_player_dept_username` (`dept_id`, `username`)'
|
||||||
|
);
|
||||||
|
echo "已创建 uk_dice_player_dept_username\n";
|
||||||
|
} else {
|
||||||
|
echo "uk_dice_player_dept_username 已存在,跳过\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "完成\n";
|
||||||
60
server/db/run_dice_reward_config_tier_recommend_menu.php
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* 执行档位结算推荐配置菜单权限 SQL
|
||||||
|
* 用法:在 server 目录执行 php db/run_dice_reward_config_tier_recommend_menu.php
|
||||||
|
*/
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
define('BASE_PATH', dirname(__DIR__));
|
||||||
|
|
||||||
|
require_once BASE_PATH . '/vendor/autoload.php';
|
||||||
|
|
||||||
|
if (class_exists(\Dotenv\Dotenv::class) && is_file(BASE_PATH . '/.env')) {
|
||||||
|
if (method_exists(\Dotenv\Dotenv::class, 'createUnsafeMutable')) {
|
||||||
|
\Dotenv\Dotenv::createUnsafeMutable(BASE_PATH)->load();
|
||||||
|
} else {
|
||||||
|
\Dotenv\Dotenv::createMutable(BASE_PATH)->load();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$host = getenv('DB_HOST') ?: '127.0.0.1';
|
||||||
|
$port = getenv('DB_PORT') ?: '3306';
|
||||||
|
$db = getenv('DB_NAME') ?: '';
|
||||||
|
$user = getenv('DB_USER') ?: '';
|
||||||
|
$pass = getenv('DB_PASSWORD') ?: '';
|
||||||
|
$dsn = "mysql:host={$host};port={$port};dbname={$db};charset=utf8mb4";
|
||||||
|
$pdo = new PDO($dsn, $user, $pass, [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);
|
||||||
|
|
||||||
|
$sqlFile = __DIR__ . '/dice_reward_config_tier_recommend_menu.sql';
|
||||||
|
$sql = file_get_contents($sqlFile);
|
||||||
|
$sql = preg_replace('/--.*$/m', '', $sql);
|
||||||
|
$parts = array_filter(array_map('trim', explode(';', $sql)));
|
||||||
|
|
||||||
|
echo "执行: dice_reward_config_tier_recommend_menu.sql\n";
|
||||||
|
echo "数据库: {$db} @ {$host}\n\n";
|
||||||
|
|
||||||
|
foreach ($parts as $statement) {
|
||||||
|
if ($statement === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$pdo->exec($statement);
|
||||||
|
}
|
||||||
|
|
||||||
|
$row = $pdo->query(
|
||||||
|
"SELECT id, parent_id, name, slug FROM sa_system_menu WHERE slug = 'dice:reward_config:index:tierRecommend' AND type = 3 LIMIT 1"
|
||||||
|
)->fetch(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
if ($row) {
|
||||||
|
echo "成功:已写入菜单权限\n";
|
||||||
|
echo " id={$row['id']} parent_id={$row['parent_id']} name={$row['name']} slug={$row['slug']}\n";
|
||||||
|
} else {
|
||||||
|
$parent = $pdo->query(
|
||||||
|
"SELECT id, name, path FROM sa_system_menu WHERE type = 2 AND (path = 'reward_config' OR component LIKE '%reward_config%') ORDER BY id ASC LIMIT 1"
|
||||||
|
)->fetch(PDO::FETCH_ASSOC);
|
||||||
|
if ($parent === false) {
|
||||||
|
echo "失败:未找到「奖励配置」父菜单 (type=2),请先创建 reward_config 菜单\n";
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
echo "警告:权限 slug 未查到(可能已存在但未插入)。父菜单: id={$parent['id']} path={$parent['path']}\n";
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
33
server/db/run_fill_missing_channel_config.php
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* 为已有渠道补齐缺失的默认配置(不删除已有数据)
|
||||||
|
* 用法:php db/run_fill_missing_channel_config.php
|
||||||
|
*/
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
define('BASE_PATH', dirname(__DIR__));
|
||||||
|
require_once BASE_PATH . '/vendor/autoload.php';
|
||||||
|
|
||||||
|
if (class_exists(\Dotenv\Dotenv::class) && is_file(BASE_PATH . '/.env')) {
|
||||||
|
\Dotenv\Dotenv::createUnsafeMutable(BASE_PATH)->load();
|
||||||
|
}
|
||||||
|
\Webman\Config::load(BASE_PATH . '/config', ['route', 'plugin']);
|
||||||
|
\Webman\ThinkOrm\ThinkOrm::start(null);
|
||||||
|
|
||||||
|
use app\dice\service\DiceChannelConfigService;
|
||||||
|
use plugin\saiadmin\app\model\system\SystemDept;
|
||||||
|
|
||||||
|
echo "========== 补齐渠道缺失配置 ==========\n";
|
||||||
|
|
||||||
|
$service = new DiceChannelConfigService();
|
||||||
|
$service->ensureConfigCompositeKeys();
|
||||||
|
|
||||||
|
$summary = $service->syncAllChannelsFromDefault();
|
||||||
|
foreach ($summary as $deptId => $info) {
|
||||||
|
$copied = implode(',', $info['copied'] ?? []) ?: '无';
|
||||||
|
$merged = empty($info['merged']) ? '无' : json_encode($info['merged'], JSON_UNESCAPED_UNICODE);
|
||||||
|
$skipped = implode(',', $info['skipped'] ?? []) ?: '无';
|
||||||
|
echo "渠道 {$deptId}: 新增表 [{$copied}] 补齐行 {$merged} 跳过 [{$skipped}]\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "\n渠道数: " . SystemDept::count() . "\n完成。\n";
|
||||||
100
server/db/run_remove_safeguard_ops_role_menus.php
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* 从所有角色中移除以下运维菜单及其按钮权限:
|
||||||
|
* /safeguard/dict
|
||||||
|
* /safeguard/attachment
|
||||||
|
* /safeguard/database
|
||||||
|
* /safeguard/server
|
||||||
|
* /safeguard/cache
|
||||||
|
* /safeguard/email-log
|
||||||
|
*
|
||||||
|
* 用法(在 server 目录): php db/run_remove_safeguard_ops_role_menus.php
|
||||||
|
*/
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
require_once __DIR__ . '/../vendor/autoload.php';
|
||||||
|
require_once __DIR__ . '/../support/bootstrap.php';
|
||||||
|
|
||||||
|
use plugin\saiadmin\app\cache\UserMenuCache;
|
||||||
|
use support\think\Db;
|
||||||
|
|
||||||
|
$targetRoutes = [
|
||||||
|
'/safeguard/dict',
|
||||||
|
'/safeguard/attachment',
|
||||||
|
'/safeguard/database',
|
||||||
|
'/safeguard/server',
|
||||||
|
'/safeguard/cache',
|
||||||
|
'/safeguard/email-log',
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param int[] $parentIds
|
||||||
|
* @return int[]
|
||||||
|
*/
|
||||||
|
function collectChildMenuIds(array $parentIds): array
|
||||||
|
{
|
||||||
|
if ($parentIds === []) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$childIds = Db::name('sa_system_menu')
|
||||||
|
->whereIn('parent_id', $parentIds)
|
||||||
|
->column('id');
|
||||||
|
|
||||||
|
if ($childIds === []) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$childIds = array_map('intval', $childIds);
|
||||||
|
$deeper = collectChildMenuIds($childIds);
|
||||||
|
|
||||||
|
return array_values(array_unique(array_merge($childIds, $deeper)));
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "=== remove safeguard ops menus from all roles ===\n";
|
||||||
|
|
||||||
|
$rootMenuIds = Db::name('sa_system_menu')
|
||||||
|
->whereIn('component', $targetRoutes)
|
||||||
|
->column('id');
|
||||||
|
|
||||||
|
$rootMenuIds = array_map('intval', $rootMenuIds ?: []);
|
||||||
|
$childMenuIds = collectChildMenuIds($rootMenuIds);
|
||||||
|
$menuIds = array_values(array_unique(array_merge($rootMenuIds, $childMenuIds)));
|
||||||
|
|
||||||
|
if ($menuIds === []) {
|
||||||
|
echo "WARN: no menu matched target routes\n";
|
||||||
|
exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
$menuRows = Db::name('sa_system_menu')
|
||||||
|
->whereIn('id', $menuIds)
|
||||||
|
->field('id, parent_id, name, component, slug, type')
|
||||||
|
->order('id', 'asc')
|
||||||
|
->select()
|
||||||
|
->toArray();
|
||||||
|
|
||||||
|
echo "Matched menus (" . count($menuRows) . "):\n";
|
||||||
|
foreach ($menuRows as $row) {
|
||||||
|
echo sprintf(
|
||||||
|
" - id=%d parent=%d type=%s component=%s slug=%s name=%s\n",
|
||||||
|
(int) ($row['id'] ?? 0),
|
||||||
|
(int) ($row['parent_id'] ?? 0),
|
||||||
|
(string) ($row['type'] ?? ''),
|
||||||
|
(string) ($row['component'] ?? ''),
|
||||||
|
(string) ($row['slug'] ?? ''),
|
||||||
|
(string) ($row['name'] ?? '')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$deleted = Db::name('sa_system_role_menu')->whereIn('menu_id', $menuIds)->delete();
|
||||||
|
echo "Deleted role-menu rows: {$deleted}\n";
|
||||||
|
|
||||||
|
$roleCount = Db::name('sa_system_role_menu')
|
||||||
|
->whereIn('menu_id', $menuIds)
|
||||||
|
->count();
|
||||||
|
echo "Remaining role-menu rows for these menus: {$roleCount}\n";
|
||||||
|
|
||||||
|
UserMenuCache::clearMenuCache();
|
||||||
|
echo "Cleared menu cache\n";
|
||||||
|
|
||||||
|
echo "Done.\n";
|
||||||
74
server/db/run_sa_system_role_dept_id.php
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* 执行 sa_system_role 渠道隔离迁移,并为已有渠道复制默认角色、映射用户角色
|
||||||
|
*
|
||||||
|
* 用法(在 server 目录): php db/run_sa_system_role_dept_id.php
|
||||||
|
*/
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
require_once __DIR__ . '/../vendor/autoload.php';
|
||||||
|
require_once __DIR__ . '/../support/bootstrap.php';
|
||||||
|
|
||||||
|
use plugin\saiadmin\app\service\SystemRoleChannelService;
|
||||||
|
use support\think\Db;
|
||||||
|
|
||||||
|
function tableHasColumn(string $table, string $column): bool
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$fields = Db::getFields($table);
|
||||||
|
return isset($fields[$column]);
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function indexExists(string $table, string $indexName): bool
|
||||||
|
{
|
||||||
|
$rows = Db::query("SHOW INDEX FROM `{$table}` WHERE Key_name = ?", [$indexName]);
|
||||||
|
return !empty($rows);
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "=== sa_system_role dept_id migration ===\n";
|
||||||
|
|
||||||
|
if (!tableHasColumn('sa_system_role', 'dept_id')) {
|
||||||
|
Db::execute(
|
||||||
|
"ALTER TABLE `sa_system_role`
|
||||||
|
ADD COLUMN `dept_id` bigint(20) UNSIGNED NOT NULL DEFAULT 0 COMMENT '所属渠道ID,0=默认模板' AFTER `id`"
|
||||||
|
);
|
||||||
|
echo "OK: ADD COLUMN dept_id\n";
|
||||||
|
} else {
|
||||||
|
echo "SKIP: dept_id column exists\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!indexExists('sa_system_role', 'idx_dept_id')) {
|
||||||
|
Db::execute('ALTER TABLE `sa_system_role` ADD INDEX `idx_dept_id` (`dept_id`)');
|
||||||
|
echo "OK: ADD INDEX idx_dept_id\n";
|
||||||
|
} else {
|
||||||
|
echo "SKIP: idx_dept_id exists\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
Db::execute('UPDATE `sa_system_role` SET `dept_id` = 0 WHERE `id` > 1');
|
||||||
|
echo "OK: UPDATE template dept_id\n";
|
||||||
|
|
||||||
|
if (indexExists('sa_system_role', 'uk_slug')) {
|
||||||
|
Db::execute('ALTER TABLE `sa_system_role` DROP INDEX `uk_slug`');
|
||||||
|
echo "OK: DROP INDEX uk_slug\n";
|
||||||
|
} else {
|
||||||
|
echo "SKIP: uk_slug not found\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!indexExists('sa_system_role', 'uk_dept_code')) {
|
||||||
|
Db::execute('ALTER TABLE `sa_system_role` ADD UNIQUE KEY `uk_dept_code` (`dept_id`, `code`)');
|
||||||
|
echo "OK: ADD UNIQUE uk_dept_code\n";
|
||||||
|
} else {
|
||||||
|
echo "SKIP: uk_dept_code exists\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
$service = new SystemRoleChannelService();
|
||||||
|
$sync = $service->syncAllChannelsFromDefault();
|
||||||
|
echo "Synced roles for channels: " . json_encode($sync, JSON_UNESCAPED_UNICODE) . "\n";
|
||||||
|
|
||||||
|
$mapped = $service->remapUserRolesToChannelRoles();
|
||||||
|
echo "Remapped user roles: {$mapped}\n";
|
||||||
|
|
||||||
|
echo "Done.\n";
|
||||||
58
server/db/run_sync_channel_config.php
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* 仅执行渠道配置同步(步骤 4-6),适用于已完成表结构迁移后
|
||||||
|
* 用法:php db/run_sync_channel_config.php
|
||||||
|
*/
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
define('BASE_PATH', dirname(__DIR__));
|
||||||
|
require_once BASE_PATH . '/vendor/autoload.php';
|
||||||
|
|
||||||
|
if (class_exists(\Dotenv\Dotenv::class) && is_file(BASE_PATH . '/.env')) {
|
||||||
|
\Dotenv\Dotenv::createUnsafeMutable(BASE_PATH)->load();
|
||||||
|
}
|
||||||
|
\Webman\Config::load(BASE_PATH . '/config', ['route', 'plugin']);
|
||||||
|
\Webman\ThinkOrm\ThinkOrm::start(null);
|
||||||
|
|
||||||
|
use app\dice\service\DiceChannelConfigService;
|
||||||
|
use plugin\saiadmin\app\model\system\SystemDept;
|
||||||
|
use support\think\Db;
|
||||||
|
|
||||||
|
echo "========== 渠道配置同步 ==========\n";
|
||||||
|
|
||||||
|
$service = new DiceChannelConfigService();
|
||||||
|
echo "调整配置表主键与默认模板 dept_id=0...\n";
|
||||||
|
$service->ensureConfigCompositeKeys();
|
||||||
|
|
||||||
|
$tables = ['dice_config', 'dice_ante_config', 'dice_lottery_pool_config', 'dice_reward_config', 'dice_reward', 'dice_game'];
|
||||||
|
|
||||||
|
echo "\n清理各渠道不完整配置后重新复制...\n";
|
||||||
|
$deptIds = SystemDept::column('id');
|
||||||
|
foreach ($deptIds as $deptId) {
|
||||||
|
$deptId = (int) $deptId;
|
||||||
|
if ($deptId <= 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
foreach ($tables as $table) {
|
||||||
|
try {
|
||||||
|
Db::table($table)->where('dept_id', $deptId)->delete();
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "设为默认模板 (dept_id=0)...\n";
|
||||||
|
foreach ($tables as $table) {
|
||||||
|
Db::table($table)->whereNull('dept_id')->update(['dept_id' => 0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "从默认模板复制到各渠道...\n";
|
||||||
|
$summary = $service->syncAllChannelsFromDefault();
|
||||||
|
foreach ($summary as $deptId => $info) {
|
||||||
|
echo "渠道 {$deptId}: 复制 [" . (implode(',', $info['copied'] ?? []) ?: '无') . "] 跳过 [" . (implode(',', $info['skipped'] ?? []) ?: '无') . "]\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "\n回填 dept_id...\n";
|
||||||
|
print_r($service->backfillDataDeptId());
|
||||||
|
|
||||||
|
echo "\n渠道数: " . SystemDept::count() . "\n完成。\n";
|
||||||
107
server/db/run_system_admin_guide_menu.php
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* 安装「后台操作指南」菜单与权限,并授权给超级管理员角色
|
||||||
|
* 用法(在 server 目录): php db/run_system_admin_guide_menu.php
|
||||||
|
*/
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
require_once __DIR__ . '/../vendor/autoload.php';
|
||||||
|
require_once __DIR__ . '/../support/bootstrap.php';
|
||||||
|
|
||||||
|
use plugin\saiadmin\app\cache\UserMenuCache;
|
||||||
|
use support\think\Db;
|
||||||
|
|
||||||
|
function runSqlFile(PDO $pdo, string $path, string $label): void
|
||||||
|
{
|
||||||
|
echo "\n=== {$label} ===\n";
|
||||||
|
if (! is_file($path)) {
|
||||||
|
echo "跳过:文件不存在 {$path}\n";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$sql = file_get_contents($path);
|
||||||
|
$sql = preg_replace('/--.*$/m', '', $sql);
|
||||||
|
$parts = array_filter(array_map('trim', explode(';', $sql)));
|
||||||
|
$ok = 0;
|
||||||
|
foreach ($parts as $statement) {
|
||||||
|
if ($statement === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$pdo->exec($statement);
|
||||||
|
$ok++;
|
||||||
|
}
|
||||||
|
echo "完成:执行 {$ok} 条语句\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
function cliPdo(): PDO
|
||||||
|
{
|
||||||
|
$host = getenv('DB_HOST') ?: '127.0.0.1';
|
||||||
|
$port = getenv('DB_PORT') ?: '3306';
|
||||||
|
$db = getenv('DB_NAME') ?: '';
|
||||||
|
$user = getenv('DB_USER') ?: '';
|
||||||
|
$pass = getenv('DB_PASSWORD') ?: '';
|
||||||
|
$dsn = "mysql:host={$host};port={$port};dbname={$db};charset=utf8mb4";
|
||||||
|
return new PDO($dsn, $user, $pass, [
|
||||||
|
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "========== 后台操作指南菜单安装 ==========\n";
|
||||||
|
echo '数据库: ' . (getenv('DB_NAME') ?: '') . '@' . (getenv('DB_HOST') ?: '') . "\n";
|
||||||
|
|
||||||
|
$pdo = cliPdo();
|
||||||
|
runSqlFile($pdo, __DIR__ . '/system_admin_guide_menu.sql', '1. 菜单与按钮权限');
|
||||||
|
|
||||||
|
$menuId = (int) Db::name('sa_system_menu')
|
||||||
|
->where('path', 'admin_guide')
|
||||||
|
->where('component', '/system/admin_guide/index')
|
||||||
|
->where('type', 2)
|
||||||
|
->value('id');
|
||||||
|
|
||||||
|
if ($menuId <= 0) {
|
||||||
|
echo "错误:未找到后台操作指南菜单\n";
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
$buttonIds = Db::name('sa_system_menu')
|
||||||
|
->where('parent_id', $menuId)
|
||||||
|
->where('type', 3)
|
||||||
|
->column('id');
|
||||||
|
|
||||||
|
$allMenuIds = array_values(array_unique(array_merge([$menuId], array_map('intval', $buttonIds ?: []))));
|
||||||
|
|
||||||
|
echo "\n=== 2. 授权超级管理员角色 ===\n";
|
||||||
|
$adminRoleIds = Db::name('sa_system_role')
|
||||||
|
->where('code', 'super_admin')
|
||||||
|
->column('id');
|
||||||
|
|
||||||
|
if ($adminRoleIds === [] || $adminRoleIds === null) {
|
||||||
|
$adminRoleIds = Db::name('sa_system_role')->where('id', 1)->column('id');
|
||||||
|
}
|
||||||
|
|
||||||
|
$inserted = 0;
|
||||||
|
foreach ($adminRoleIds as $roleId) {
|
||||||
|
$roleId = (int) $roleId;
|
||||||
|
foreach ($allMenuIds as $mid) {
|
||||||
|
$exists = Db::name('sa_system_role_menu')
|
||||||
|
->where('role_id', $roleId)
|
||||||
|
->where('menu_id', $mid)
|
||||||
|
->count();
|
||||||
|
if ($exists > 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Db::name('sa_system_role_menu')->insert([
|
||||||
|
'role_id' => $roleId,
|
||||||
|
'menu_id' => $mid,
|
||||||
|
]);
|
||||||
|
$inserted++;
|
||||||
|
}
|
||||||
|
echo " 角色 {$roleId}:新增授权 {$inserted} 条\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
UserMenuCache::clearMenuCache();
|
||||||
|
\plugin\saiadmin\app\cache\UserAuthCache::clear();
|
||||||
|
|
||||||
|
echo "\n菜单 ID: {$menuId}\n";
|
||||||
|
echo "按钮权限: " . implode(',', $allMenuIds) . "\n";
|
||||||
|
echo "已清除菜单缓存,请重新登录后台查看。\n";
|
||||||
|
echo "========== 安装完成 ==========\n";
|
||||||
85
server/db/run_system_log_dept_id.php
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* 为登录日志、操作日志补充 dept_id 字段并回填历史数据。
|
||||||
|
*
|
||||||
|
* 用法(在 server 目录): php db/run_system_log_dept_id.php
|
||||||
|
*/
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
require_once __DIR__ . '/../vendor/autoload.php';
|
||||||
|
require_once __DIR__ . '/../support/bootstrap.php';
|
||||||
|
|
||||||
|
use support\think\Db;
|
||||||
|
|
||||||
|
function systemLogTableHasColumn(string $table, string $column): bool
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$fields = Db::getFields($table);
|
||||||
|
return isset($fields[$column]);
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function systemLogIndexExists(string $table, string $indexName): bool
|
||||||
|
{
|
||||||
|
$rows = Db::query("SHOW INDEX FROM `{$table}` WHERE Key_name = ?", [$indexName]);
|
||||||
|
return !empty($rows);
|
||||||
|
}
|
||||||
|
|
||||||
|
function addLogDeptColumn(string $table, string $afterColumn): void
|
||||||
|
{
|
||||||
|
if (!systemLogTableHasColumn($table, 'dept_id')) {
|
||||||
|
Db::execute(
|
||||||
|
"ALTER TABLE `{$table}`
|
||||||
|
ADD COLUMN `dept_id` bigint(20) UNSIGNED NULL DEFAULT NULL COMMENT '所属渠道ID' AFTER `{$afterColumn}`"
|
||||||
|
);
|
||||||
|
echo "OK: {$table} ADD COLUMN dept_id\n";
|
||||||
|
} else {
|
||||||
|
echo "SKIP: {$table} dept_id column exists\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!systemLogIndexExists($table, 'idx_dept_id')) {
|
||||||
|
Db::execute("ALTER TABLE `{$table}` ADD INDEX `idx_dept_id` (`dept_id`)");
|
||||||
|
echo "OK: {$table} ADD INDEX idx_dept_id\n";
|
||||||
|
} else {
|
||||||
|
echo "SKIP: {$table} idx_dept_id exists\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "=== system log dept_id migration ===\n";
|
||||||
|
|
||||||
|
addLogDeptColumn('sa_system_login_log', 'login_time');
|
||||||
|
addLogDeptColumn('sa_system_oper_log', 'request_data');
|
||||||
|
|
||||||
|
$loginByCreator = Db::execute(
|
||||||
|
"UPDATE `sa_system_login_log` l
|
||||||
|
INNER JOIN `sa_system_user` u ON u.id = l.created_by
|
||||||
|
SET l.dept_id = u.dept_id
|
||||||
|
WHERE (l.dept_id IS NULL OR l.dept_id = 0)
|
||||||
|
AND u.dept_id IS NOT NULL
|
||||||
|
AND u.dept_id > 0"
|
||||||
|
);
|
||||||
|
echo "OK: login log backfilled by created_by: {$loginByCreator}\n";
|
||||||
|
|
||||||
|
$loginByUsername = Db::execute(
|
||||||
|
"UPDATE `sa_system_login_log` l
|
||||||
|
INNER JOIN `sa_system_user` u ON u.username = l.username
|
||||||
|
SET l.dept_id = u.dept_id
|
||||||
|
WHERE (l.dept_id IS NULL OR l.dept_id = 0)
|
||||||
|
AND u.dept_id IS NOT NULL
|
||||||
|
AND u.dept_id > 0"
|
||||||
|
);
|
||||||
|
echo "OK: login log backfilled by username: {$loginByUsername}\n";
|
||||||
|
|
||||||
|
$operByUsername = Db::execute(
|
||||||
|
"UPDATE `sa_system_oper_log` o
|
||||||
|
INNER JOIN `sa_system_user` u ON u.username = o.username
|
||||||
|
SET o.dept_id = u.dept_id
|
||||||
|
WHERE (o.dept_id IS NULL OR o.dept_id = 0)
|
||||||
|
AND u.dept_id IS NOT NULL
|
||||||
|
AND u.dept_id > 0"
|
||||||
|
);
|
||||||
|
echo "OK: oper log backfilled by username: {$operByUsername}\n";
|
||||||
|
|
||||||
|
echo "Done.\n";
|
||||||
10
server/db/sa_system_role_add_dept_id.sql
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
-- 角色表按渠道隔离:dept_id=0 为默认模板角色,各渠道拥有独立角色副本
|
||||||
|
|
||||||
|
ALTER TABLE `sa_system_role`
|
||||||
|
ADD COLUMN `dept_id` bigint(20) UNSIGNED NOT NULL DEFAULT 0 COMMENT '所属渠道ID,0=默认模板' AFTER `id`,
|
||||||
|
ADD INDEX `idx_dept_id` (`dept_id`);
|
||||||
|
|
||||||
|
UPDATE `sa_system_role` SET `dept_id` = 0 WHERE `dept_id` IS NULL OR `id` > 1;
|
||||||
|
|
||||||
|
ALTER TABLE `sa_system_role` DROP INDEX `uk_slug`;
|
||||||
|
ALTER TABLE `sa_system_role` ADD UNIQUE KEY `uk_dept_code` (`dept_id`, `code`);
|
||||||
21
server/db/sync_channel_default_roles.php
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* 为各渠道补齐三个默认代理角色,并清理多余角色(无用户绑定的)
|
||||||
|
* 用法: php db/sync_channel_default_roles.php
|
||||||
|
*/
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
require_once __DIR__ . '/../vendor/autoload.php';
|
||||||
|
require_once __DIR__ . '/../support/bootstrap.php';
|
||||||
|
|
||||||
|
use plugin\saiadmin\app\service\SystemRoleChannelService;
|
||||||
|
|
||||||
|
$service = new SystemRoleChannelService();
|
||||||
|
echo 'Default role codes: ' . implode(', ', $service->getDefaultChannelRoleCodes()) . "\n";
|
||||||
|
|
||||||
|
$sync = $service->syncAllChannelsFromDefault();
|
||||||
|
echo json_encode($sync, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT) . "\n";
|
||||||
|
|
||||||
|
$mapped = $service->remapUserRolesToChannelRoles();
|
||||||
|
echo "Remapped user roles: {$mapped}\n";
|
||||||
|
echo "Done.\n";
|
||||||
39
server/db/system_admin_guide_menu.sql
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
-- 后台操作指南顶级菜单与权限
|
||||||
|
-- 说明:挂载到顶级菜单(parent_id=0),内容来源 server/docs/ADMIN_GUIDE.md
|
||||||
|
|
||||||
|
SET @now = NOW();
|
||||||
|
|
||||||
|
-- 1) 创建后台操作指南顶级菜单(type=2,parent_id=0)
|
||||||
|
INSERT INTO `sa_system_menu`
|
||||||
|
(`parent_id`,`name`,`code`,`slug`,`type`,`path`,`component`,`method`,`icon`,`sort`,`is_iframe`,`is_keep_alive`,`is_hidden`,`is_fixed_tab`,`is_full_page`,`generate_id`,`generate_key`,`status`,`create_time`,`update_time`)
|
||||||
|
SELECT 0, '后台操作指南', 'AdminGuide', NULL, 2, 'admin_guide', '/system/admin_guide/index', NULL, 'ri:book-read-line', 5, 2, 2, 2, 2, 2, 0, NULL, 1, @now, @now
|
||||||
|
WHERE NOT EXISTS (
|
||||||
|
SELECT 1 FROM `sa_system_menu` WHERE `path` = 'admin_guide' AND `component` = '/system/admin_guide/index' AND `type` = 2
|
||||||
|
);
|
||||||
|
|
||||||
|
SET @admin_guide_menu_id = (
|
||||||
|
SELECT `id` FROM `sa_system_menu`
|
||||||
|
WHERE `path` = 'admin_guide' AND `component` = '/system/admin_guide/index' AND `type` = 2
|
||||||
|
ORDER BY `id` ASC LIMIT 1
|
||||||
|
);
|
||||||
|
|
||||||
|
-- 2) 创建按钮权限
|
||||||
|
INSERT INTO `sa_system_menu`
|
||||||
|
(`parent_id`,`name`,`code`,`slug`,`type`,`path`,`component`,`method`,`sort`,`is_iframe`,`is_keep_alive`,`is_hidden`,`is_fixed_tab`,`is_full_page`,`generate_id`,`generate_key`,`status`,`create_time`,`update_time`)
|
||||||
|
SELECT @admin_guide_menu_id, '数据列表', '', 'system:admin_guide:index:index', 3, '', '', '', 100, 2, 2, 2, 2, 2, 0, NULL, 1, @now, @now
|
||||||
|
WHERE NOT EXISTS (SELECT 1 FROM `sa_system_menu` WHERE `slug` = 'system:admin_guide:index:index' AND `type` = 3);
|
||||||
|
|
||||||
|
INSERT INTO `sa_system_menu`
|
||||||
|
(`parent_id`,`name`,`code`,`slug`,`type`,`path`,`component`,`method`,`sort`,`is_iframe`,`is_keep_alive`,`is_hidden`,`is_fixed_tab`,`is_full_page`,`generate_id`,`generate_key`,`status`,`create_time`,`update_time`)
|
||||||
|
SELECT @admin_guide_menu_id, '读取', '', 'system:admin_guide:index:read', 3, '', '', '', 100, 2, 2, 2, 2, 2, 0, NULL, 1, @now, @now
|
||||||
|
WHERE NOT EXISTS (SELECT 1 FROM `sa_system_menu` WHERE `slug` = 'system:admin_guide:index:read' AND `type` = 3);
|
||||||
|
|
||||||
|
INSERT INTO `sa_system_menu`
|
||||||
|
(`parent_id`,`name`,`code`,`slug`,`type`,`path`,`component`,`method`,`sort`,`is_iframe`,`is_keep_alive`,`is_hidden`,`is_fixed_tab`,`is_full_page`,`generate_id`,`generate_key`,`status`,`create_time`,`update_time`)
|
||||||
|
SELECT @admin_guide_menu_id, '编辑', '', 'system:admin_guide:index:edit', 3, '', '', '', 100, 2, 2, 2, 2, 2, 0, NULL, 1, @now, @now
|
||||||
|
WHERE NOT EXISTS (SELECT 1 FROM `sa_system_menu` WHERE `slug` = 'system:admin_guide:index:edit' AND `type` = 3);
|
||||||
|
|
||||||
|
INSERT INTO `sa_system_menu`
|
||||||
|
(`parent_id`,`name`,`code`,`slug`,`type`,`path`,`component`,`method`,`sort`,`is_iframe`,`is_keep_alive`,`is_hidden`,`is_fixed_tab`,`is_full_page`,`generate_id`,`generate_key`,`status`,`create_time`,`update_time`)
|
||||||
|
SELECT @admin_guide_menu_id, '保存', '', 'system:admin_guide:index:save', 3, '', '', '', 100, 2, 2, 2, 2, 2, 0, NULL, 1, @now, @now
|
||||||
|
WHERE NOT EXISTS (SELECT 1 FROM `sa_system_menu` WHERE `slug` = 'system:admin_guide:index:save' AND `type` = 3);
|
||||||
20
server/db/test_reward_config_list.php
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
define('BASE_PATH', dirname(__DIR__));
|
||||||
|
require BASE_PATH . '/vendor/autoload.php';
|
||||||
|
\Dotenv\Dotenv::createUnsafeMutable(BASE_PATH)->load();
|
||||||
|
\Webman\Config::load(BASE_PATH . '/config', ['route', 'plugin']);
|
||||||
|
\Webman\ThinkOrm\ThinkOrm::start(null);
|
||||||
|
|
||||||
|
use app\dice\helper\AdminScopeHelper;
|
||||||
|
use app\dice\model\reward_config\DiceRewardConfig;
|
||||||
|
use app\dice\logic\reward_config\DiceRewardConfigLogic;
|
||||||
|
|
||||||
|
$deptId = 1123;
|
||||||
|
$query = (new DiceRewardConfig())->order('id', 'asc');
|
||||||
|
AdminScopeHelper::applyConfigScope($query, null, $deptId);
|
||||||
|
$logic = new DiceRewardConfigLogic();
|
||||||
|
$result = $logic->getList($query);
|
||||||
|
echo 'keys: ' . implode(',', array_keys($result)) . "\n";
|
||||||
|
$data = $result['data'] ?? $result['records'] ?? [];
|
||||||
|
echo 'count: ' . (is_array($data) ? count($data) : 0) . "\n";
|
||||||
18
server/db/test_reward_config_scope.php
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
define('BASE_PATH', dirname(__DIR__));
|
||||||
|
require BASE_PATH . '/vendor/autoload.php';
|
||||||
|
\Dotenv\Dotenv::createUnsafeMutable(BASE_PATH)->load();
|
||||||
|
\Webman\Config::load(BASE_PATH . '/config', ['route', 'plugin']);
|
||||||
|
\Webman\ThinkOrm\ThinkOrm::start(null);
|
||||||
|
|
||||||
|
use app\dice\helper\AdminScopeHelper;
|
||||||
|
use app\dice\model\reward_config\DiceRewardConfig;
|
||||||
|
|
||||||
|
foreach ([0, 1123] as $deptId) {
|
||||||
|
$q1 = DiceRewardConfig::where('dept_id', $deptId);
|
||||||
|
echo "direct dept {$deptId}: " . $q1->count() . "\n";
|
||||||
|
$q2 = (new DiceRewardConfig())->order('id', 'asc');
|
||||||
|
AdminScopeHelper::applyConfigScope($q2, ['id' => 1], $deptId);
|
||||||
|
echo "super admin scoped dept {$deptId}: " . $q2->count() . "\n";
|
||||||
|
}
|
||||||
45
server/db/verify_channel_init.php
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
define('BASE_PATH', dirname(__DIR__));
|
||||||
|
require_once BASE_PATH . '/vendor/autoload.php';
|
||||||
|
\Dotenv\Dotenv::createUnsafeMutable(BASE_PATH)->load();
|
||||||
|
\Webman\Config::load(BASE_PATH . '/config', ['route', 'plugin']);
|
||||||
|
\Webman\ThinkOrm\ThinkOrm::start(null);
|
||||||
|
|
||||||
|
use plugin\saiadmin\app\model\system\SystemDept;
|
||||||
|
use support\think\Db;
|
||||||
|
|
||||||
|
$deptIds = SystemDept::column('id');
|
||||||
|
echo "渠道数: " . count($deptIds) . " [" . implode(',', $deptIds) . "]\n\n";
|
||||||
|
|
||||||
|
$tables = [
|
||||||
|
'dice_config',
|
||||||
|
'dice_ante_config',
|
||||||
|
'dice_lottery_pool_config',
|
||||||
|
'dice_reward_config',
|
||||||
|
'dice_reward',
|
||||||
|
'dice_game',
|
||||||
|
];
|
||||||
|
|
||||||
|
echo "=== 配置按 dept_id 统计 ===\n";
|
||||||
|
foreach ($tables as $table) {
|
||||||
|
$rows = Db::query("SELECT dept_id, COUNT(*) AS cnt FROM `{$table}` GROUP BY dept_id ORDER BY dept_id");
|
||||||
|
echo "{$table}:\n";
|
||||||
|
foreach ($rows as $r) {
|
||||||
|
$dept = $r['dept_id'] === null ? 'NULL' : (string) $r['dept_id'];
|
||||||
|
echo " dept_id={$dept}: {$r['cnt']}\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "\n=== 业务数据未回填 dept_id ===\n";
|
||||||
|
$biz = ['dice_player', 'dice_play_record', 'dice_play_record_test'];
|
||||||
|
foreach ($biz as $table) {
|
||||||
|
if (!Db::getFields($table)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$nullCnt = Db::table($table)->where(function ($q) {
|
||||||
|
$q->whereNull('dept_id')->whereOr('dept_id', 0);
|
||||||
|
})->count();
|
||||||
|
$total = Db::table($table)->count();
|
||||||
|
echo "{$table}: 未回填 {$nullCnt} / 总计 {$total}\n";
|
||||||
|
}
|
||||||
26
server/db/verify_reward_index_all.php
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
define('BASE_PATH', dirname(__DIR__));
|
||||||
|
require BASE_PATH . '/vendor/autoload.php';
|
||||||
|
\Dotenv\Dotenv::createUnsafeMutable(BASE_PATH)->load();
|
||||||
|
\Webman\Config::load(BASE_PATH . '/config', ['route', 'plugin']);
|
||||||
|
\Webman\ThinkOrm\ThinkOrm::start(null);
|
||||||
|
|
||||||
|
use app\dice\helper\AdminScopeHelper;
|
||||||
|
use app\dice\logic\reward_config\DiceRewardConfigLogic;
|
||||||
|
use plugin\saiadmin\app\cache\UserInfoCache;
|
||||||
|
|
||||||
|
$adminInfo = UserInfoCache::getUserInfo(123);
|
||||||
|
$logic = new DiceRewardConfigLogic();
|
||||||
|
$query = $logic->search([]);
|
||||||
|
AdminScopeHelper::applyConfigScope($query, $adminInfo, 1123);
|
||||||
|
$data = $query->order('id', 'asc')->select()->toArray();
|
||||||
|
echo 'controller-style all rows: ' . count($data) . PHP_EOL;
|
||||||
|
$nonBig = 0;
|
||||||
|
foreach ($data as $row) {
|
||||||
|
if (($row['tier'] ?? '') !== 'BIGWIN') {
|
||||||
|
$nonBig++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
echo 'index tab rows (non-BIGWIN): ' . $nonBig . PHP_EOL;
|
||||||
|
echo 'bigwin tab rows: ' . (count($data) - $nonBig) . PHP_EOL;
|
||||||
128
server/docs/ADMIN_GUIDE.md
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
# 大富翁-使用说明指南
|
||||||
|
|
||||||
|
## 菜单简单介绍
|
||||||
|
|
||||||
|
### 工作台/统计页面:统计数据
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### 角色管理:对角色的菜单权限设置
|
||||||
|
|
||||||
|
按等级设定,等级越低权限越少(不要出现上级角色没有的权限,子角色有)
|
||||||
|
避免方式:使用子角色创建下级角色,可以避免下级角色比上级角色操作权限更多的问题
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
这里设置角色的菜单以及按钮权限
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### 彩金池配置:监听彩金池实时变化
|
||||||
|
|
||||||
|
可以实时监听彩金池累积金额的变化
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### 游戏配置:游戏规则和平台币转化比
|
||||||
|
|
||||||
|
游戏配置
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
其中游戏玩法为进入游戏的弹窗,和规则介绍(无特殊需求不需要大改)
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
游戏平台币兑换币:为进入平台时平台比转化比,比如,当前设置的为1:1,如果从jk8平台转入100,那么获取的游戏币为100,如果设置1:2则获取的平台币为200
|
||||||
|
|
||||||
|
### 底注配置:方便玩家快速调整压注倍率
|
||||||
|
|
||||||
|
底注配置
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
对应游戏中的,其中每次游玩对局基础消耗为1游戏币(无法修改),底注的设置只是方便玩家快速修改压注金额
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## 抽奖逻辑
|
||||||
|
|
||||||
|
### 判断抽奖档位
|
||||||
|
|
||||||
|
当前的抽奖逻辑时,按照抽奖档位(T1-T5)进行抽奖,在【玩家管理】菜单中的设置玩家具体的档位权重
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
也可以在【彩金池配置】菜单中设置玩家正常的抽奖档位权重
|
||||||
|
|
||||||
|
- 其中正常的档位权重为注册玩家默认绑定的档位权重,并且只有在修改完后,创建的玩家才能绑定最新的正常档位权重
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
- 其中free为杀分权重为如果当前彩金池(平台)盈利超过设置的安全线则制动走杀分的权重
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
- 剩余的两个权重可以方便快速切换用户的档位抽奖权重
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### 根据档位抽取中奖号码
|
||||||
|
|
||||||
|
#### 设置中奖号码地图
|
||||||
|
|
||||||
|
在后台设置地图缩影
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
地图的索引参看如下
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
其中地图的索引可以按照需求点击图中的按规则生成
|
||||||
|
|
||||||
|
并且规则尽可能符合:结算金额>2 → T1;2>=结算金额>1 → T2;1>=结算金额>0 → T3;0>结算金额 → T4(惩罚);0=结算金额 → T5(再来一次)
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
#### 创建完地图索引后创建相应的奖励对照表
|
||||||
|
|
||||||
|
创建奖励对照表的原因是由于有每个号码的权重不一样,豹子号10,15,20,25有多重组合方式,所以需要设置奖励对照表中的权重配比
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
根据抽到的奖励档位,抽取号码(主要用于设置抽取豹子号的5,10,15,20,25,30的权重)
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
比如上图中如果不设置,色子点数5抽到的概率和其他点数的概率是一样的,可能抽7次T1奖励,就有1次中豹子号5的可能
|
||||||
|
|
||||||
|
由于抽到色子点数和为10,15,20,25的色子点数组合有多种,所以在抽该这四个点数时还需要单独配置相应的中大奖概率(其中豹子号5和30只有一种组合【1,1,1,1,1】和【6,6,6,6,6】,所以不需要配置),其中权重拉到最大10000,那么中奖概率为100%(只要摇到了相应的色子点数和则中奖概率为100%)
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
#### 扩展
|
||||||
|
|
||||||
|
- 测试设置的中奖概率,根据如下设置可以测试当前设置权重的中奖概率,该测试数据不记录到真实数据系统中
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
能够准确的反馈抽中点数的统计
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
如果当前中奖概率符合预期,或则测试多组数据选取一组符合预期的导入到当前的配置中
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
这里可以详情查询到指定测试记录的详情,
|
||||||
|
|
||||||
|

|
||||||
@@ -26,7 +26,12 @@
|
|||||||
- 请求头统一:
|
- 请求头统一:
|
||||||
- `Content-Type: application/json`
|
- `Content-Type: application/json`
|
||||||
- `Accept: application/json`
|
- `Accept: application/json`
|
||||||
|
- `api-key: {api_key}`(**所有 `/api/v1/*` 必传**,与服务端 `.env` 中 `API_KEY` 一致)
|
||||||
- `auth-token: {authtoken}`(除 `/api/v1/authToken` 外必传)
|
- `auth-token: {authtoken}`(除 `/api/v1/authToken` 外必传)
|
||||||
|
- `api-key` 携带方式(任选其一,按优先级读取,先命中即采用):
|
||||||
|
1. 请求头 `api-key`(**推荐**)
|
||||||
|
2. URL 查询参数 `api_key`(或 `api-key`)
|
||||||
|
3. body 表单/JSON 字段 `api_key`(或 `api-key`)
|
||||||
- 时间相关参数统一使用 Unix 时间戳(秒)
|
- 时间相关参数统一使用 Unix 时间戳(秒)
|
||||||
- 建议所有请求设置超时:连接超时 `3s`,读取超时 `10s`
|
- 建议所有请求设置超时:连接超时 `3s`,读取超时 `10s`
|
||||||
- 生产环境建议增加调用方 IP 白名单和重试退避机制(避免瞬时重试风暴)
|
- 生产环境建议增加调用方 IP 白名单和重试退避机制(避免瞬时重试风暴)
|
||||||
@@ -53,9 +58,9 @@
|
|||||||
常见错误码:
|
常见错误码:
|
||||||
|
|
||||||
- `400` 参数错误
|
- `400` 参数错误
|
||||||
- `401` 未携带 token
|
- `401` 未携带 `api-key`、`auth-token` 或 `token`
|
||||||
- `402` token 无效或过期
|
- `402` token 无效或过期
|
||||||
- `403` 签名或鉴权失败
|
- `403` `api-key` 无效、签名或鉴权失败
|
||||||
- `404` 资源不存在
|
- `404` 资源不存在
|
||||||
- `422` 业务错误(如余额不足)
|
- `422` 业务错误(如余额不足)
|
||||||
- `500` 服务端异常
|
- `500` 服务端异常
|
||||||
@@ -64,11 +69,18 @@
|
|||||||
|
|
||||||
## 4. 鉴权流程(平台级)
|
## 4. 鉴权流程(平台级)
|
||||||
|
|
||||||
|
平台级凭证分两层:
|
||||||
|
|
||||||
|
- **`api-key`**:所有 `/api/v1/*` 接口必传,与服务端 `.env` 中 `API_KEY` 一致;可通过请求头、query、body 任一方式携带(详见 §2.1)。
|
||||||
|
- **`auth-token`**:业务接口(除 `/api/v1/authToken` 外)必传,由 `/api/v1/authToken` 颁发。
|
||||||
|
|
||||||
`/api/v1/*` 接口调用前,先获取 `auth-token`。
|
`/api/v1/*` 接口调用前,先获取 `auth-token`。
|
||||||
|
|
||||||
### 4.1 获取 auth-token
|
### 4.1 获取 auth-token
|
||||||
|
|
||||||
- 路径: `GET /api/v1/authToken`
|
- 路径: `GET /api/v1/authToken`
|
||||||
|
- Header:
|
||||||
|
- `api-key: {api_key}`(必传,与服务端 `.env` 中 `API_KEY` 一致)
|
||||||
- 鉴权参数(Query):
|
- 鉴权参数(Query):
|
||||||
- `agent_id`:代理标识(商户标识)
|
- `agent_id`:代理标识(商户标识)
|
||||||
- `secret`:双方约定密钥
|
- `secret`:双方约定密钥
|
||||||
@@ -113,11 +125,12 @@ const signature = crypto.createHash('md5').update(agentId + secret + time).diges
|
|||||||
|
|
||||||
服务端校验逻辑(关键点):
|
服务端校验逻辑(关键点):
|
||||||
|
|
||||||
|
- `api-key` 缺失即失败(`401`),与 `.env` 中 `API_KEY` 不一致即失败(`403`)
|
||||||
- `agent_id/secret/time/signature` 任一缺失即失败(`400`)
|
- `agent_id/secret/time/signature` 任一缺失即失败(`400`)
|
||||||
- `secret` 不匹配即失败(`403`)
|
- `secret` 不匹配即失败(`403`)
|
||||||
- `time` 超出容差窗口即失败(`403`,默认容差 `300s`)
|
- `time` 超出容差窗口即失败(`403`,默认容差 `300s`)
|
||||||
- `signature` 校验失败即失败(`403`)
|
- `signature` 校验失败即失败(`403`)
|
||||||
- 校验通过后颁发 `authtoken`,后续请求必须放在 Header `auth-token`
|
- 校验通过后颁发 `authtoken`,后续请求必须放在 Header `auth-token`(同时仍需带 `api-key`)
|
||||||
|
|
||||||
防重放与时间同步建议:
|
防重放与时间同步建议:
|
||||||
|
|
||||||
@@ -140,14 +153,15 @@ const signature = crypto.createHash('md5').update(agentId + secret + time).diges
|
|||||||
后续调用 `/api/v1/*` 时,请在 Header 携带:
|
后续调用 `/api/v1/*` 时,请在 Header 携带:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
|
api-key: {api_key}
|
||||||
auth-token: {authtoken}
|
auth-token: {authtoken}
|
||||||
```
|
```
|
||||||
|
|
||||||
### 4.2 完整调用链(推荐)
|
### 4.2 完整调用链(推荐)
|
||||||
|
|
||||||
1. 计算 `signature = md5(agent_id + secret + time)`
|
1. 计算 `signature = md5(agent_id + secret + time)`
|
||||||
2. 调用 `GET /api/v1/authToken` 获取 `authtoken`
|
2. 调用 `GET /api/v1/authToken`(Header 携带 `api-key`)获取 `authtoken`
|
||||||
3. 在 Header 添加 `auth-token: {authtoken}`
|
3. 在 Header 添加 `api-key: {api_key}` 与 `auth-token: {authtoken}`
|
||||||
4. 调用业务接口(如 `getPlayerInfo`、`setPlayerWallet`、`getGameUrl`、`getPlayerGameRecord`、`getPlayerWalletRecord`、`getPlayerTicketRecord`)
|
4. 调用业务接口(如 `getPlayerInfo`、`setPlayerWallet`、`getGameUrl`、`getPlayerGameRecord`、`getPlayerWalletRecord`、`getPlayerTicketRecord`)
|
||||||
5. 若返回 `402`,重新获取 `authtoken` 后重试一次
|
5. 若返回 `402`,重新获取 `authtoken` 后重试一次
|
||||||
|
|
||||||
@@ -155,12 +169,13 @@ auth-token: {authtoken}
|
|||||||
|
|
||||||
## 5. 游戏相关接口
|
## 5. 游戏相关接口
|
||||||
|
|
||||||
以下接口均需 Header: `auth-token`。
|
以下接口均需 Header:`api-key` + `auth-token`(`api-key` 也可放 query/body,参见 §2.1)。
|
||||||
|
|
||||||
## 5.1 获取游戏列表(已支持)
|
## 5.1 获取游戏列表(已支持)
|
||||||
|
|
||||||
- 路径: `POST /api/v1/getGameList`
|
- 路径: `POST /api/v1/getGameList`
|
||||||
- Header:
|
- Header:
|
||||||
|
- `api-key: {api_key}`
|
||||||
- `auth-token: {authtoken}`
|
- `auth-token: {authtoken}`
|
||||||
- Body 参数:
|
- Body 参数:
|
||||||
- `lang`(可选):`zh`/`en`,默认 `zh`
|
- `lang`(可选):`zh`/`en`,默认 `zh`
|
||||||
@@ -240,6 +255,7 @@ auth-token: {authtoken}
|
|||||||
|
|
||||||
- 路径: `POST /api/v1/getGameHall`
|
- 路径: `POST /api/v1/getGameHall`
|
||||||
- Header:
|
- Header:
|
||||||
|
- `api-key: {api_key}`
|
||||||
- `auth-token: {authtoken}`
|
- `auth-token: {authtoken}`
|
||||||
- Body 参数:
|
- Body 参数:
|
||||||
- `lang`(可选):`zh`/`en`,默认 `zh`
|
- `lang`(可选):`zh`/`en`,默认 `zh`
|
||||||
@@ -316,9 +332,11 @@ auth-token: {authtoken}
|
|||||||
## 5.3 获取某个游戏地址(已支持)
|
## 5.3 获取某个游戏地址(已支持)
|
||||||
|
|
||||||
- 路径: `POST /api/v1/getGameUrl`
|
- 路径: `POST /api/v1/getGameUrl`
|
||||||
|
- Header:
|
||||||
|
- `api-key: {api_key}`
|
||||||
|
- `auth-token: {authtoken}`
|
||||||
- Body 参数:
|
- Body 参数:
|
||||||
- `username`(必填):玩家账号(不存在会自动创建)
|
- `username`(必填):玩家账号(不存在会自动创建)
|
||||||
- `password`(可选):默认 `123456`
|
|
||||||
- `time`(可选):不传则服务端取当前时间戳
|
- `time`(可选):不传则服务端取当前时间戳
|
||||||
- `lang`(可选):`zh`/`en`,默认 `zh`
|
- `lang`(可选):`zh`/`en`,默认 `zh`
|
||||||
|
|
||||||
@@ -349,6 +367,7 @@ auth-token: {authtoken}
|
|||||||
|
|
||||||
- 路径: `POST /api/v1/getPlayerGameRecord`
|
- 路径: `POST /api/v1/getPlayerGameRecord`
|
||||||
- Header:
|
- Header:
|
||||||
|
- `api-key: {api_key}`
|
||||||
- `auth-token: {authtoken}`
|
- `auth-token: {authtoken}`
|
||||||
- Body 参数:
|
- Body 参数:
|
||||||
- `username`(可选):玩家账号;不传则**不按玩家筛选**(返回库内符合条件的记录,请谨慎使用)
|
- `username`(可选):玩家账号;不传则**不按玩家筛选**(返回库内符合条件的记录,请谨慎使用)
|
||||||
@@ -396,7 +415,7 @@ auth-token: {authtoken}
|
|||||||
|
|
||||||
## 7. 钱包相关接口
|
## 7. 钱包相关接口
|
||||||
|
|
||||||
以下接口均需 Header: `auth-token`。
|
以下接口均需 Header:`api-key` + `auth-token`(`api-key` 也可放 query/body,参见 §2.1)。
|
||||||
|
|
||||||
### 7.1 查询余额(已支持)
|
### 7.1 查询余额(已支持)
|
||||||
|
|
||||||
@@ -430,6 +449,7 @@ auth-token: {authtoken}
|
|||||||
|
|
||||||
- 路径: `POST /api/v1/getPlayerWalletRecord`
|
- 路径: `POST /api/v1/getPlayerWalletRecord`
|
||||||
- Header:
|
- Header:
|
||||||
|
- `api-key: {api_key}`
|
||||||
- `auth-token: {authtoken}`
|
- `auth-token: {authtoken}`
|
||||||
- Body 参数:
|
- Body 参数:
|
||||||
- `username`(可选):玩家账号;不传则**不按玩家筛选**
|
- `username`(可选):玩家账号;不传则**不按玩家筛选**
|
||||||
@@ -445,6 +465,7 @@ auth-token: {authtoken}
|
|||||||
|
|
||||||
- 路径: `POST /api/v1/getPlayerTicketRecord`
|
- 路径: `POST /api/v1/getPlayerTicketRecord`
|
||||||
- Header:
|
- Header:
|
||||||
|
- `api-key: {api_key}`
|
||||||
- `auth-token: {authtoken}`
|
- `auth-token: {authtoken}`
|
||||||
- Body 参数:与 **7.4** 相同(`username`、`start_create_time`、`end_create_time`、`limit`)
|
- Body 参数:与 **7.4** 相同(`username`、`start_create_time`、`end_create_time`、`limit`)
|
||||||
- 返回说明:
|
- 返回说明:
|
||||||
@@ -462,7 +483,8 @@ auth-token: {authtoken}
|
|||||||
- `provider`:`Dicey Fun`
|
- `provider`:`Dicey Fun`
|
||||||
- `provider_code`:`DF`
|
- `provider_code`:`DF`
|
||||||
- `agent_id`:`5ef059938ba799aaa845e1c2e8a762bd`
|
- `agent_id`:`5ef059938ba799aaa845e1c2e8a762bd`
|
||||||
- `secret`:签名密钥(双方约定)
|
- `secret`:签名密钥(双方约定,对应服务端 `.env` 中 `API_AUTH_TOKEN_SECRET`)
|
||||||
|
- `api_key`:所有 `/api/v1/*` 请求必传的 `api-key`(对应服务端 `.env` 中 `API_KEY`)
|
||||||
- `agent_token`:`[我来填]`(如需额外业务层 token)
|
- `agent_token`:`[我来填]`(如需额外业务层 token)
|
||||||
- `game_url`:游戏前端域名/地址
|
- `game_url`:游戏前端域名/地址
|
||||||
- `lobby_url`:大厅地址(可选)
|
- `lobby_url`:大厅地址(可选)
|
||||||
@@ -482,8 +504,8 @@ auth-token: {authtoken}
|
|||||||
|
|
||||||
## 10. 对接时序(建议)
|
## 10. 对接时序(建议)
|
||||||
|
|
||||||
1. 平台分配 `agent_id`、`secret`
|
1. 平台分配 `agent_id`、`secret`、`api_key`
|
||||||
2. 第三方调用 `/api/v1/authToken` 获取 `authtoken`
|
2. 第三方调用 `/api/v1/authToken`(Header 携带 `api-key`)获取 `authtoken`
|
||||||
3. 第三方调用 `/api/v1/getGameHall` 或 `/api/v1/getGameList` 获取大厅/游戏信息
|
3. 第三方调用 `/api/v1/getGameHall` 或 `/api/v1/getGameList` 获取大厅/游戏信息
|
||||||
4. 第三方调用 `/api/v1/getPlayerInfo`(可选,检查用户与余额)
|
4. 第三方调用 `/api/v1/getPlayerInfo`(可选,检查用户与余额)
|
||||||
5. 第三方调用 `/api/v1/setPlayerWallet` 进行额度转入(如有)
|
5. 第三方调用 `/api/v1/setPlayerWallet` 进行额度转入(如有)
|
||||||
@@ -498,7 +520,8 @@ auth-token: {authtoken}
|
|||||||
### 11.1 获取 auth-token
|
### 11.1 获取 auth-token
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
curl --location --request GET 'https://{your-domain}/api/v1/authToken?agent_id={agent_id}&secret={secret}&time={time}&signature={signature}'
|
curl --location --request GET 'https://{your-domain}/api/v1/authToken?agent_id={agent_id}&secret={secret}&time={time}&signature={signature}' \
|
||||||
|
--header 'api-key: {api_key}'
|
||||||
```
|
```
|
||||||
|
|
||||||
建议在接入测试时,先本地打印以下值再发请求,便于排查:
|
建议在接入测试时,先本地打印以下值再发请求,便于排查:
|
||||||
@@ -514,6 +537,7 @@ curl --location --request GET 'https://{your-domain}/api/v1/authToken?agent_id={
|
|||||||
```bash
|
```bash
|
||||||
curl --location --request POST 'https://{your-domain}/api/v1/getGameUrl' \
|
curl --location --request POST 'https://{your-domain}/api/v1/getGameUrl' \
|
||||||
--header 'Content-Type: application/json' \
|
--header 'Content-Type: application/json' \
|
||||||
|
--header 'api-key: {api_key}' \
|
||||||
--header 'auth-token: {authtoken}' \
|
--header 'auth-token: {authtoken}' \
|
||||||
--data-raw '{
|
--data-raw '{
|
||||||
"username":"test_player_001",
|
"username":"test_player_001",
|
||||||
@@ -526,6 +550,7 @@ curl --location --request POST 'https://{your-domain}/api/v1/getGameUrl' \
|
|||||||
```bash
|
```bash
|
||||||
curl --location --request POST 'https://{your-domain}/api/v1/getGameList' \
|
curl --location --request POST 'https://{your-domain}/api/v1/getGameList' \
|
||||||
--header 'Content-Type: application/json' \
|
--header 'Content-Type: application/json' \
|
||||||
|
--header 'api-key: {api_key}' \
|
||||||
--header 'auth-token: {authtoken}' \
|
--header 'auth-token: {authtoken}' \
|
||||||
--data-raw '{
|
--data-raw '{
|
||||||
"lang":"zh"
|
"lang":"zh"
|
||||||
@@ -537,6 +562,7 @@ curl --location --request POST 'https://{your-domain}/api/v1/getGameList' \
|
|||||||
```bash
|
```bash
|
||||||
curl --location --request POST 'https://{your-domain}/api/v1/getGameList' \
|
curl --location --request POST 'https://{your-domain}/api/v1/getGameList' \
|
||||||
--header 'Content-Type: application/json' \
|
--header 'Content-Type: application/json' \
|
||||||
|
--header 'api-key: {api_key}' \
|
||||||
--header 'auth-token: {authtoken}' \
|
--header 'auth-token: {authtoken}' \
|
||||||
--data-raw '{
|
--data-raw '{
|
||||||
"lang":"en"
|
"lang":"en"
|
||||||
@@ -548,6 +574,7 @@ curl --location --request POST 'https://{your-domain}/api/v1/getGameList' \
|
|||||||
```bash
|
```bash
|
||||||
curl --location --request POST 'https://{your-domain}/api/v1/getGameHall' \
|
curl --location --request POST 'https://{your-domain}/api/v1/getGameHall' \
|
||||||
--header 'Content-Type: application/json' \
|
--header 'Content-Type: application/json' \
|
||||||
|
--header 'api-key: {api_key}' \
|
||||||
--header 'auth-token: {authtoken}' \
|
--header 'auth-token: {authtoken}' \
|
||||||
--data-raw '{
|
--data-raw '{
|
||||||
"lang":"zh"
|
"lang":"zh"
|
||||||
@@ -559,6 +586,7 @@ curl --location --request POST 'https://{your-domain}/api/v1/getGameHall' \
|
|||||||
```bash
|
```bash
|
||||||
curl --location --request POST 'https://{your-domain}/api/v1/setPlayerWallet' \
|
curl --location --request POST 'https://{your-domain}/api/v1/setPlayerWallet' \
|
||||||
--header 'Content-Type: application/json' \
|
--header 'Content-Type: application/json' \
|
||||||
|
--header 'api-key: {api_key}' \
|
||||||
--header 'auth-token: {authtoken}' \
|
--header 'auth-token: {authtoken}' \
|
||||||
--data-raw '{
|
--data-raw '{
|
||||||
"username":"test_player_001",
|
"username":"test_player_001",
|
||||||
@@ -571,6 +599,7 @@ curl --location --request POST 'https://{your-domain}/api/v1/setPlayerWallet' \
|
|||||||
```bash
|
```bash
|
||||||
curl --location --request POST 'https://{your-domain}/api/v1/getPlayerInfo' \
|
curl --location --request POST 'https://{your-domain}/api/v1/getPlayerInfo' \
|
||||||
--header 'Content-Type: application/json' \
|
--header 'Content-Type: application/json' \
|
||||||
|
--header 'api-key: {api_key}' \
|
||||||
--header 'auth-token: {authtoken}' \
|
--header 'auth-token: {authtoken}' \
|
||||||
--data-raw '{
|
--data-raw '{
|
||||||
"username":"test_player_001"
|
"username":"test_player_001"
|
||||||
@@ -582,6 +611,7 @@ curl --location --request POST 'https://{your-domain}/api/v1/getPlayerInfo' \
|
|||||||
```bash
|
```bash
|
||||||
curl --location --request POST 'https://{your-domain}/api/v1/getPlayerGameRecord' \
|
curl --location --request POST 'https://{your-domain}/api/v1/getPlayerGameRecord' \
|
||||||
--header 'Content-Type: application/json' \
|
--header 'Content-Type: application/json' \
|
||||||
|
--header 'api-key: {api_key}' \
|
||||||
--header 'auth-token: {authtoken}' \
|
--header 'auth-token: {authtoken}' \
|
||||||
--data-raw '{
|
--data-raw '{
|
||||||
"username":"test_player_001",
|
"username":"test_player_001",
|
||||||
@@ -594,6 +624,7 @@ curl --location --request POST 'https://{your-domain}/api/v1/getPlayerGameRecord
|
|||||||
```bash
|
```bash
|
||||||
curl --location --request POST 'https://{your-domain}/api/v1/getPlayerWalletRecord' \
|
curl --location --request POST 'https://{your-domain}/api/v1/getPlayerWalletRecord' \
|
||||||
--header 'Content-Type: application/json' \
|
--header 'Content-Type: application/json' \
|
||||||
|
--header 'api-key: {api_key}' \
|
||||||
--header 'auth-token: {authtoken}' \
|
--header 'auth-token: {authtoken}' \
|
||||||
--data-raw '{
|
--data-raw '{
|
||||||
"username":"test_player_001",
|
"username":"test_player_001",
|
||||||
@@ -606,6 +637,7 @@ curl --location --request POST 'https://{your-domain}/api/v1/getPlayerWalletReco
|
|||||||
```bash
|
```bash
|
||||||
curl --location --request POST 'https://{your-domain}/api/v1/getPlayerTicketRecord' \
|
curl --location --request POST 'https://{your-domain}/api/v1/getPlayerTicketRecord' \
|
||||||
--header 'Content-Type: application/json' \
|
--header 'Content-Type: application/json' \
|
||||||
|
--header 'api-key: {api_key}' \
|
||||||
--header 'auth-token: {authtoken}' \
|
--header 'auth-token: {authtoken}' \
|
||||||
--data-raw '{
|
--data-raw '{
|
||||||
"username":"test_player_001",
|
"username":"test_player_001",
|
||||||
|
|||||||
@@ -26,7 +26,12 @@
|
|||||||
- Unified headers:
|
- Unified headers:
|
||||||
- `Content-Type: application/json`
|
- `Content-Type: application/json`
|
||||||
- `Accept: application/json`
|
- `Accept: application/json`
|
||||||
|
- `api-key: {api_key}` (**Required for ALL `/api/v1/*` endpoints**, must match `API_KEY` in server `.env`)
|
||||||
- `auth-token: {authtoken}` (Required for all endpoints except `/api/v1/authToken`)
|
- `auth-token: {authtoken}` (Required for all endpoints except `/api/v1/authToken`)
|
||||||
|
- `api-key` may be supplied in any of the following ways (read in priority order, first non-empty wins):
|
||||||
|
1. HTTP header `api-key` (**recommended**)
|
||||||
|
2. Query string `api_key` (or `api-key`)
|
||||||
|
3. Body form/JSON field `api_key` (or `api-key`)
|
||||||
- All time-related parameters use Unix timestamps (seconds)
|
- All time-related parameters use Unix timestamps (seconds)
|
||||||
- Recommended timeouts: connect timeout `3s`, read timeout `10s`
|
- Recommended timeouts: connect timeout `3s`, read timeout `10s`
|
||||||
- Production recommendation: add caller IP whitelist and retry backoff (to avoid burst retry storms)
|
- Production recommendation: add caller IP whitelist and retry backoff (to avoid burst retry storms)
|
||||||
@@ -53,9 +58,9 @@ Notes:
|
|||||||
Common error codes:
|
Common error codes:
|
||||||
|
|
||||||
- `400` Invalid parameters
|
- `400` Invalid parameters
|
||||||
- `401` Missing token
|
- `401` Missing `api-key`, `auth-token` or `token`
|
||||||
- `402` Token invalid or expired
|
- `402` Token invalid or expired
|
||||||
- `403` Signature or authentication failed
|
- `403` Invalid `api-key`, signature or authentication failed
|
||||||
- `404` Resource not found
|
- `404` Resource not found
|
||||||
- `422` Business error (e.g., insufficient balance)
|
- `422` Business error (e.g., insufficient balance)
|
||||||
- `500` Server exception
|
- `500` Server exception
|
||||||
@@ -64,11 +69,18 @@ Common error codes:
|
|||||||
|
|
||||||
## 4. Authentication Flow (Platform Level)
|
## 4. Authentication Flow (Platform Level)
|
||||||
|
|
||||||
|
Two layers of platform-level credentials:
|
||||||
|
|
||||||
|
- **`api-key`**: Required for ALL `/api/v1/*` endpoints, must match `API_KEY` in server `.env`. May be sent in header, query, or body (see §2.1).
|
||||||
|
- **`auth-token`**: Required for business endpoints (i.e., everything except `/api/v1/authToken`); obtained from `/api/v1/authToken`.
|
||||||
|
|
||||||
Before calling any `/api/v1/*` endpoint, obtain an `auth-token` first.
|
Before calling any `/api/v1/*` endpoint, obtain an `auth-token` first.
|
||||||
|
|
||||||
### 4.1 Get auth-token
|
### 4.1 Get auth-token
|
||||||
|
|
||||||
- Path: `GET /api/v1/authToken`
|
- Path: `GET /api/v1/authToken`
|
||||||
|
- Header:
|
||||||
|
- `api-key: {api_key}` (Required, must match `API_KEY` in server `.env`)
|
||||||
- Auth parameters (Query):
|
- Auth parameters (Query):
|
||||||
- `agent_id`: Agent identifier (merchant identifier)
|
- `agent_id`: Agent identifier (merchant identifier)
|
||||||
- `secret`: Shared secret agreed by both parties
|
- `secret`: Shared secret agreed by both parties
|
||||||
@@ -113,11 +125,12 @@ const signature = crypto.createHash('md5').update(agentId + secret + time).diges
|
|||||||
|
|
||||||
Server-side validation logic (key points):
|
Server-side validation logic (key points):
|
||||||
|
|
||||||
|
- Missing `api-key` => fail (`401`); `api-key` not equal to `.env` `API_KEY` => fail (`403`)
|
||||||
- Missing any of `agent_id/secret/time/signature` => fail (`400`)
|
- Missing any of `agent_id/secret/time/signature` => fail (`400`)
|
||||||
- `secret` mismatch => fail (`403`)
|
- `secret` mismatch => fail (`403`)
|
||||||
- `time` outside tolerance window => fail (`403`, default tolerance `300s`)
|
- `time` outside tolerance window => fail (`403`, default tolerance `300s`)
|
||||||
- `signature` mismatch => fail (`403`)
|
- `signature` mismatch => fail (`403`)
|
||||||
- If validated, the server issues `authtoken`; subsequent requests must include it in the `auth-token` header
|
- If validated, the server issues `authtoken`; subsequent requests must include it in the `auth-token` header (and still carry `api-key`)
|
||||||
|
|
||||||
Anti-replay and time sync recommendations:
|
Anti-replay and time sync recommendations:
|
||||||
|
|
||||||
@@ -137,17 +150,18 @@ Success response example:
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
For subsequent calls to `/api/v1/*`, include the following header:
|
For subsequent calls to `/api/v1/*`, include the following headers:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
|
api-key: {api_key}
|
||||||
auth-token: {authtoken}
|
auth-token: {authtoken}
|
||||||
```
|
```
|
||||||
|
|
||||||
### 4.2 Full Call Chain (Recommended)
|
### 4.2 Full Call Chain (Recommended)
|
||||||
|
|
||||||
1. Compute `signature = md5(agent_id + secret + time)`
|
1. Compute `signature = md5(agent_id + secret + time)`
|
||||||
2. Call `GET /api/v1/authToken` to obtain `authtoken`
|
2. Call `GET /api/v1/authToken` (Header `api-key`) to obtain `authtoken`
|
||||||
3. Add header `auth-token: {authtoken}`
|
3. Add headers `api-key: {api_key}` and `auth-token: {authtoken}`
|
||||||
4. Call business endpoints (e.g., `getPlayerInfo`, `setPlayerWallet`, `getGameUrl`, `getPlayerGameRecord`, `getPlayerWalletRecord`, `getPlayerTicketRecord`)
|
4. Call business endpoints (e.g., `getPlayerInfo`, `setPlayerWallet`, `getGameUrl`, `getPlayerGameRecord`, `getPlayerWalletRecord`, `getPlayerTicketRecord`)
|
||||||
5. If `402` is returned, re-fetch `authtoken` and retry once
|
5. If `402` is returned, re-fetch `authtoken` and retry once
|
||||||
|
|
||||||
@@ -155,12 +169,13 @@ auth-token: {authtoken}
|
|||||||
|
|
||||||
## 5. Game APIs
|
## 5. Game APIs
|
||||||
|
|
||||||
All endpoints below require the `auth-token` header.
|
All endpoints below require headers `api-key` + `auth-token` (`api-key` may also be sent via query/body, see §2.1).
|
||||||
|
|
||||||
## 5.1 Get Game List (Supported)
|
## 5.1 Get Game List (Supported)
|
||||||
|
|
||||||
- Path: `POST /api/v1/getGameList`
|
- Path: `POST /api/v1/getGameList`
|
||||||
- Header:
|
- Header:
|
||||||
|
- `api-key: {api_key}`
|
||||||
- `auth-token: {authtoken}`
|
- `auth-token: {authtoken}`
|
||||||
- Body parameters:
|
- Body parameters:
|
||||||
- `lang` (optional): `zh`/`en`, default `zh`
|
- `lang` (optional): `zh`/`en`, default `zh`
|
||||||
@@ -240,6 +255,7 @@ Success example (`lang=en`):
|
|||||||
|
|
||||||
- Path: `POST /api/v1/getGameHall`
|
- Path: `POST /api/v1/getGameHall`
|
||||||
- Header:
|
- Header:
|
||||||
|
- `api-key: {api_key}`
|
||||||
- `auth-token: {authtoken}`
|
- `auth-token: {authtoken}`
|
||||||
- Body parameters:
|
- Body parameters:
|
||||||
- `lang` (optional): `zh`/`en`, default `zh`
|
- `lang` (optional): `zh`/`en`, default `zh`
|
||||||
@@ -316,9 +332,11 @@ Success example (`lang=en`):
|
|||||||
## 5.3 Get Game URL (Supported)
|
## 5.3 Get Game URL (Supported)
|
||||||
|
|
||||||
- Path: `POST /api/v1/getGameUrl`
|
- Path: `POST /api/v1/getGameUrl`
|
||||||
|
- Header:
|
||||||
|
- `api-key: {api_key}`
|
||||||
|
- `auth-token: {authtoken}`
|
||||||
- Body parameters:
|
- Body parameters:
|
||||||
- `username` (required): Player username (auto-created if not exists)
|
- `username` (required): Player username (auto-created if not exists)
|
||||||
- `password` (optional): default `123456`
|
|
||||||
- `time` (optional): if omitted, server uses current timestamp
|
- `time` (optional): if omitted, server uses current timestamp
|
||||||
- `lang` (optional): `zh`/`en`, default `zh`
|
- `lang` (optional): `zh`/`en`, default `zh`
|
||||||
|
|
||||||
@@ -349,6 +367,7 @@ An independent endpoint is provided: `POST /api/v1/getGameList`, supporting both
|
|||||||
|
|
||||||
- Path: `POST /api/v1/getPlayerGameRecord`
|
- Path: `POST /api/v1/getPlayerGameRecord`
|
||||||
- Header:
|
- Header:
|
||||||
|
- `api-key: {api_key}`
|
||||||
- `auth-token: {authtoken}`
|
- `auth-token: {authtoken}`
|
||||||
- Body parameters:
|
- Body parameters:
|
||||||
- `username` (optional): Player username; if omitted, **no player filter** is applied (returns matching rows from the database—use with care)
|
- `username` (optional): Player username; if omitted, **no player filter** is applied (returns matching rows from the database—use with care)
|
||||||
@@ -396,7 +415,7 @@ This update introduces a game management table and menu to centrally manage basi
|
|||||||
|
|
||||||
## 7. Wallet APIs
|
## 7. Wallet APIs
|
||||||
|
|
||||||
All endpoints below require the `auth-token` header.
|
All endpoints below require headers `api-key` + `auth-token` (`api-key` may also be sent via query/body, see §2.1).
|
||||||
|
|
||||||
### 7.1 Query Balance (Supported)
|
### 7.1 Query Balance (Supported)
|
||||||
|
|
||||||
@@ -430,6 +449,7 @@ If the integrator’s wallet flow requires “return lobby URL after transfer”
|
|||||||
|
|
||||||
- Path: `POST /api/v1/getPlayerWalletRecord`
|
- Path: `POST /api/v1/getPlayerWalletRecord`
|
||||||
- Header:
|
- Header:
|
||||||
|
- `api-key: {api_key}`
|
||||||
- `auth-token: {authtoken}`
|
- `auth-token: {authtoken}`
|
||||||
- Body parameters:
|
- Body parameters:
|
||||||
- `username` (optional): Player username; if omitted, **no player filter** is applied
|
- `username` (optional): Player username; if omitted, **no player filter** is applied
|
||||||
@@ -445,6 +465,7 @@ If the integrator’s wallet flow requires “return lobby URL after transfer”
|
|||||||
|
|
||||||
- Path: `POST /api/v1/getPlayerTicketRecord`
|
- Path: `POST /api/v1/getPlayerTicketRecord`
|
||||||
- Header:
|
- Header:
|
||||||
|
- `api-key: {api_key}`
|
||||||
- `auth-token: {authtoken}`
|
- `auth-token: {authtoken}`
|
||||||
- Body parameters: Same as **7.4** (`username`, `start_create_time`, `end_create_time`, `limit`)
|
- Body parameters: Same as **7.4** (`username`, `start_create_time`, `end_create_time`, `limit`)
|
||||||
- Response notes:
|
- Response notes:
|
||||||
@@ -462,7 +483,8 @@ It is recommended to configure the following fields in the integration parameter
|
|||||||
- `provider`: `Dicey Fun`
|
- `provider`: `Dicey Fun`
|
||||||
- `provider_code`: `DF`
|
- `provider_code`: `DF`
|
||||||
- `agent_id`: `5ef059938ba799aaa845e1c2e8a762bd`
|
- `agent_id`: `5ef059938ba799aaa845e1c2e8a762bd`
|
||||||
- `secret`: Signature secret (shared by both parties)
|
- `secret`: Signature secret (shared by both parties, maps to server `.env` `API_AUTH_TOKEN_SECRET`)
|
||||||
|
- `api_key`: The `api-key` required by every `/api/v1/*` request (maps to server `.env` `API_KEY`)
|
||||||
- `agent_token`: `[to be filled by us]` (if an additional business-layer token is needed)
|
- `agent_token`: `[to be filled by us]` (if an additional business-layer token is needed)
|
||||||
- `game_url`: Game frontend domain/URL
|
- `game_url`: Game frontend domain/URL
|
||||||
- `lobby_url`: Lobby URL (optional)
|
- `lobby_url`: Lobby URL (optional)
|
||||||
@@ -482,8 +504,8 @@ It is recommended to configure the following fields in the integration parameter
|
|||||||
|
|
||||||
## 10. Integration Sequence (Recommended)
|
## 10. Integration Sequence (Recommended)
|
||||||
|
|
||||||
1. Platform assigns `agent_id` and `secret`
|
1. Platform assigns `agent_id`, `secret` and `api_key`
|
||||||
2. Third party calls `/api/v1/authToken` to obtain `authtoken`
|
2. Third party calls `/api/v1/authToken` (with header `api-key`) to obtain `authtoken`
|
||||||
3. Third party calls `/api/v1/getGameHall` or `/api/v1/getGameList` to obtain lobby/game info
|
3. Third party calls `/api/v1/getGameHall` or `/api/v1/getGameList` to obtain lobby/game info
|
||||||
4. Third party calls `/api/v1/getPlayerInfo` (optional, check user and balance)
|
4. Third party calls `/api/v1/getPlayerInfo` (optional, check user and balance)
|
||||||
5. Third party calls `/api/v1/setPlayerWallet` to credit in (if applicable)
|
5. Third party calls `/api/v1/setPlayerWallet` to credit in (if applicable)
|
||||||
@@ -498,7 +520,8 @@ It is recommended to configure the following fields in the integration parameter
|
|||||||
### 11.1 Get auth-token
|
### 11.1 Get auth-token
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
curl --location --request GET 'https://{your-domain}/api/v1/authToken?agent_id={agent_id}&secret={secret}&time={time}&signature={signature}'
|
curl --location --request GET 'https://{your-domain}/api/v1/authToken?agent_id={agent_id}&secret={secret}&time={time}&signature={signature}' \
|
||||||
|
--header 'api-key: {api_key}'
|
||||||
```
|
```
|
||||||
|
|
||||||
During integration testing, it is recommended to print the following values locally before sending the request to ease troubleshooting:
|
During integration testing, it is recommended to print the following values locally before sending the request to ease troubleshooting:
|
||||||
@@ -514,6 +537,7 @@ During integration testing, it is recommended to print the following values loca
|
|||||||
```bash
|
```bash
|
||||||
curl --location --request POST 'https://{your-domain}/api/v1/getGameUrl' \
|
curl --location --request POST 'https://{your-domain}/api/v1/getGameUrl' \
|
||||||
--header 'Content-Type: application/json' \
|
--header 'Content-Type: application/json' \
|
||||||
|
--header 'api-key: {api_key}' \
|
||||||
--header 'auth-token: {authtoken}' \
|
--header 'auth-token: {authtoken}' \
|
||||||
--data-raw '{
|
--data-raw '{
|
||||||
"username":"test_player_001",
|
"username":"test_player_001",
|
||||||
@@ -526,6 +550,7 @@ curl --location --request POST 'https://{your-domain}/api/v1/getGameUrl' \
|
|||||||
```bash
|
```bash
|
||||||
curl --location --request POST 'https://{your-domain}/api/v1/getGameList' \
|
curl --location --request POST 'https://{your-domain}/api/v1/getGameList' \
|
||||||
--header 'Content-Type: application/json' \
|
--header 'Content-Type: application/json' \
|
||||||
|
--header 'api-key: {api_key}' \
|
||||||
--header 'auth-token: {authtoken}' \
|
--header 'auth-token: {authtoken}' \
|
||||||
--data-raw '{
|
--data-raw '{
|
||||||
"lang":"zh"
|
"lang":"zh"
|
||||||
@@ -537,6 +562,7 @@ curl --location --request POST 'https://{your-domain}/api/v1/getGameList' \
|
|||||||
```bash
|
```bash
|
||||||
curl --location --request POST 'https://{your-domain}/api/v1/getGameList' \
|
curl --location --request POST 'https://{your-domain}/api/v1/getGameList' \
|
||||||
--header 'Content-Type: application/json' \
|
--header 'Content-Type: application/json' \
|
||||||
|
--header 'api-key: {api_key}' \
|
||||||
--header 'auth-token: {authtoken}' \
|
--header 'auth-token: {authtoken}' \
|
||||||
--data-raw '{
|
--data-raw '{
|
||||||
"lang":"en"
|
"lang":"en"
|
||||||
@@ -548,6 +574,7 @@ curl --location --request POST 'https://{your-domain}/api/v1/getGameList' \
|
|||||||
```bash
|
```bash
|
||||||
curl --location --request POST 'https://{your-domain}/api/v1/getGameHall' \
|
curl --location --request POST 'https://{your-domain}/api/v1/getGameHall' \
|
||||||
--header 'Content-Type: application/json' \
|
--header 'Content-Type: application/json' \
|
||||||
|
--header 'api-key: {api_key}' \
|
||||||
--header 'auth-token: {authtoken}' \
|
--header 'auth-token: {authtoken}' \
|
||||||
--data-raw '{
|
--data-raw '{
|
||||||
"lang":"zh"
|
"lang":"zh"
|
||||||
@@ -559,6 +586,7 @@ curl --location --request POST 'https://{your-domain}/api/v1/getGameHall' \
|
|||||||
```bash
|
```bash
|
||||||
curl --location --request POST 'https://{your-domain}/api/v1/setPlayerWallet' \
|
curl --location --request POST 'https://{your-domain}/api/v1/setPlayerWallet' \
|
||||||
--header 'Content-Type: application/json' \
|
--header 'Content-Type: application/json' \
|
||||||
|
--header 'api-key: {api_key}' \
|
||||||
--header 'auth-token: {authtoken}' \
|
--header 'auth-token: {authtoken}' \
|
||||||
--data-raw '{
|
--data-raw '{
|
||||||
"username":"test_player_001",
|
"username":"test_player_001",
|
||||||
@@ -571,6 +599,7 @@ curl --location --request POST 'https://{your-domain}/api/v1/setPlayerWallet' \
|
|||||||
```bash
|
```bash
|
||||||
curl --location --request POST 'https://{your-domain}/api/v1/getPlayerInfo' \
|
curl --location --request POST 'https://{your-domain}/api/v1/getPlayerInfo' \
|
||||||
--header 'Content-Type: application/json' \
|
--header 'Content-Type: application/json' \
|
||||||
|
--header 'api-key: {api_key}' \
|
||||||
--header 'auth-token: {authtoken}' \
|
--header 'auth-token: {authtoken}' \
|
||||||
--data-raw '{
|
--data-raw '{
|
||||||
"username":"test_player_001"
|
"username":"test_player_001"
|
||||||
@@ -582,6 +611,7 @@ curl --location --request POST 'https://{your-domain}/api/v1/getPlayerInfo' \
|
|||||||
```bash
|
```bash
|
||||||
curl --location --request POST 'https://{your-domain}/api/v1/getPlayerGameRecord' \
|
curl --location --request POST 'https://{your-domain}/api/v1/getPlayerGameRecord' \
|
||||||
--header 'Content-Type: application/json' \
|
--header 'Content-Type: application/json' \
|
||||||
|
--header 'api-key: {api_key}' \
|
||||||
--header 'auth-token: {authtoken}' \
|
--header 'auth-token: {authtoken}' \
|
||||||
--data-raw '{
|
--data-raw '{
|
||||||
"username":"test_player_001",
|
"username":"test_player_001",
|
||||||
@@ -594,6 +624,7 @@ curl --location --request POST 'https://{your-domain}/api/v1/getPlayerGameRecord
|
|||||||
```bash
|
```bash
|
||||||
curl --location --request POST 'https://{your-domain}/api/v1/getPlayerWalletRecord' \
|
curl --location --request POST 'https://{your-domain}/api/v1/getPlayerWalletRecord' \
|
||||||
--header 'Content-Type: application/json' \
|
--header 'Content-Type: application/json' \
|
||||||
|
--header 'api-key: {api_key}' \
|
||||||
--header 'auth-token: {authtoken}' \
|
--header 'auth-token: {authtoken}' \
|
||||||
--data-raw '{
|
--data-raw '{
|
||||||
"username":"test_player_001",
|
"username":"test_player_001",
|
||||||
@@ -606,6 +637,7 @@ curl --location --request POST 'https://{your-domain}/api/v1/getPlayerWalletReco
|
|||||||
```bash
|
```bash
|
||||||
curl --location --request POST 'https://{your-domain}/api/v1/getPlayerTicketRecord' \
|
curl --location --request POST 'https://{your-domain}/api/v1/getPlayerTicketRecord' \
|
||||||
--header 'Content-Type: application/json' \
|
--header 'Content-Type: application/json' \
|
||||||
|
--header 'api-key: {api_key}' \
|
||||||
--header 'auth-token: {authtoken}' \
|
--header 'auth-token: {authtoken}' \
|
||||||
--data-raw '{
|
--data-raw '{
|
||||||
"username":"test_player_001",
|
"username":"test_player_001",
|
||||||
|
|||||||
192
server/docs/flowcharts/dice-为何抽到该奖励.html
Normal file
@@ -0,0 +1,192 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<title>为何最终抽到该奖励</title>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.min.js"></script>
|
||||||
|
<style>
|
||||||
|
* { box-sizing: border-box; }
|
||||||
|
body {
|
||||||
|
font-family: "Microsoft YaHei", "PingFang SC", sans-serif;
|
||||||
|
margin: 0;
|
||||||
|
padding: 24px 32px 48px;
|
||||||
|
background: #f5f7fa;
|
||||||
|
color: #1a1a2e;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
header { max-width: 1100px; margin: 0 auto 16px; }
|
||||||
|
h1 { font-size: 1.5rem; margin: 0 0 8px; font-weight: 600; }
|
||||||
|
.subtitle { color: #5c6370; font-size: 0.95rem; margin: 0; }
|
||||||
|
.card {
|
||||||
|
max-width: 1100px;
|
||||||
|
margin: 0 auto;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 28px 24px;
|
||||||
|
box-shadow: 0 2px 12px rgba(0,0,0,.06);
|
||||||
|
}
|
||||||
|
.copy-hint {
|
||||||
|
max-width: 1100px;
|
||||||
|
margin: 12px auto 0;
|
||||||
|
font-size: 0.88rem;
|
||||||
|
color: #606266;
|
||||||
|
}
|
||||||
|
.copy-box {
|
||||||
|
max-width: 1100px;
|
||||||
|
margin: 8px auto 0;
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #dcdfe6;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 12px 14px;
|
||||||
|
}
|
||||||
|
.copy-box summary { cursor: pointer; font-size: 0.9rem; color: #409eff; }
|
||||||
|
.copy-box pre {
|
||||||
|
margin: 10px 0 0;
|
||||||
|
font-size: 0.78rem;
|
||||||
|
line-height: 1.45;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-all;
|
||||||
|
max-height: 280px;
|
||||||
|
overflow: auto;
|
||||||
|
color: #303133;
|
||||||
|
}
|
||||||
|
.legend {
|
||||||
|
max-width: 1100px;
|
||||||
|
margin: 20px auto 0;
|
||||||
|
padding: 16px 20px;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: #444;
|
||||||
|
}
|
||||||
|
.legend h2 { font-size: 1rem; margin: 0 0 10px; }
|
||||||
|
.legend ul { margin: 0; padding-left: 1.2em; }
|
||||||
|
.legend li { margin: 4px 0; }
|
||||||
|
.mermaid { display: flex; justify-content: center; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header>
|
||||||
|
<h1>为何最终抽到的是这个奖励</h1>
|
||||||
|
<p class="subtitle">业务说明:一局抽奖从开局到到账的决策顺序(仅用本项目菜单与业务用语)</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<p class="copy-hint">复制方式:展开下方「Mermaid 源码」全选复制,粘贴到 ProcessOn / draw.io / 飞书文档等支持 Mermaid 的流程图工具;或直接用浏览器打开本页看图。</p>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<pre class="mermaid">
|
||||||
|
flowchart TD
|
||||||
|
Start([玩家开始一局抽奖]) --> Dir[选择方向:顺时针 或 逆时针]
|
||||||
|
Dir --> Ante[选择底注倍数]
|
||||||
|
Ante --> Type{本局是否使用免费抽奖券?}
|
||||||
|
|
||||||
|
Type -->|是| Free[免费局]
|
||||||
|
Type -->|否且平台币足够| Paid[付费局:扣除底注对应平台币]
|
||||||
|
|
||||||
|
Free --> PoolKill[按「杀分奖池」的 T1~T5 档位概率抽签]
|
||||||
|
Paid --> KillCheck{彩金池已开启杀分<br/>且彩金池累计盈利 ≥ 安全线?}
|
||||||
|
KillCheck -->|是| PoolKill
|
||||||
|
KillCheck -->|否| PlayerW[按该玩家在「玩家管理」<br/>配置的 T1~T5 档位概率抽签]
|
||||||
|
|
||||||
|
PoolKill --> DrawTier[随机抽出档位 T1~T5]
|
||||||
|
PlayerW --> DrawTier
|
||||||
|
|
||||||
|
DrawTier --> PickRow[在「色子奖励权重」中<br/>取该档位 + 本局方向的所有行<br/>按行权重随机一条]
|
||||||
|
PickRow --> Got[得到:色子点数、结算金额、所属档位、落点格位]
|
||||||
|
|
||||||
|
Got --> KillMode{本局是否走杀分档位概率?}
|
||||||
|
KillMode -->|是| NoLeo[不发放豹子大奖<br/>且不会抽到仅能豹子的点数 5、30]
|
||||||
|
KillMode -->|否| NormalPath[按普通规则继续]
|
||||||
|
NoLeo --> DiceShow[生成五颗骰子并结算]
|
||||||
|
|
||||||
|
NormalPath --> Leopard{色子点数是否为<br/>5 / 10 / 15 / 20 / 25 / 30?}
|
||||||
|
Leopard -->|否| NormalWin[五颗骰子点数和 = 该点数<br/>奖金 = 结算金额 × 底注]
|
||||||
|
Leopard -->|是| LeoRule{点数?}
|
||||||
|
LeoRule -->|5 或 30| MustBig[必定豹子大奖]
|
||||||
|
LeoRule -->|10 / 15 / 20 / 25| BigRate[按「奖励配置」页签「大奖权重」<br/>该点数权重决定真豹子或普通展示]
|
||||||
|
MustBig --> BigPay[豹子奖金 = 大奖结算金额 × 底注<br/>本局不再发该点数的普通奖]
|
||||||
|
BigRate -->|命中豹子| BigPay
|
||||||
|
BigRate -->|未中豹子| NonLeo[五颗骰子为非豹子组合<br/>奖金 = 结算金额 × 底注]
|
||||||
|
|
||||||
|
NormalWin --> T5Check
|
||||||
|
NonLeo --> T5Check
|
||||||
|
BigPay --> EndBig([本局结束:以豹子大奖为准])
|
||||||
|
DiceShow --> T5Check{档位为 T5 再来一次?}
|
||||||
|
T5Check -->|是| FreeTicket[赠送 1 次免费抽奖券<br/>下次免费局须相同底注]
|
||||||
|
T5Check -->|否| EndNormal([本局结束:以普通奖或惩罚为准])
|
||||||
|
FreeTicket --> EndNormal
|
||||||
|
|
||||||
|
style Start fill:#e8f4fc
|
||||||
|
style EndNormal fill:#e8fce8
|
||||||
|
style EndBig fill:#fff3e0
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<details class="copy-box">
|
||||||
|
<summary>Mermaid 源码(可复制,与同目录 .mmd 文件一致)</summary>
|
||||||
|
<pre id="mermaid-src">flowchart TD
|
||||||
|
Start([玩家开始一局抽奖]) --> Dir[选择方向:顺时针 或 逆时针]
|
||||||
|
Dir --> Ante[选择底注倍数]
|
||||||
|
Ante --> Type{本局是否使用免费抽奖券?}
|
||||||
|
|
||||||
|
Type -->|是| Free[免费局]
|
||||||
|
Type -->|否且平台币足够| Paid[付费局:扣除底注对应平台币]
|
||||||
|
|
||||||
|
Free --> PoolKill[按「杀分奖池」的 T1~T5 档位概率抽签]
|
||||||
|
Paid --> KillCheck{彩金池已开启杀分<br/>且彩金池累计盈利 ≥ 安全线?}
|
||||||
|
KillCheck -->|是| PoolKill
|
||||||
|
KillCheck -->|否| PlayerW[按该玩家在「玩家管理」<br/>配置的 T1~T5 档位概率抽签]
|
||||||
|
|
||||||
|
PoolKill --> DrawTier[随机抽出档位 T1~T5]
|
||||||
|
PlayerW --> DrawTier
|
||||||
|
|
||||||
|
DrawTier --> PickRow[在「色子奖励权重」中<br/>取该档位 + 本局方向的所有行<br/>按行权重随机一条]
|
||||||
|
PickRow --> Got[得到:色子点数、结算金额、所属档位、落点格位]
|
||||||
|
|
||||||
|
Got --> KillMode{本局是否走杀分档位概率?}
|
||||||
|
KillMode -->|是| NoLeo[不发放豹子大奖<br/>且不会抽到仅能豹子的点数 5、30]
|
||||||
|
KillMode -->|否| NormalPath[按普通规则继续]
|
||||||
|
NoLeo --> DiceShow[生成五颗骰子并结算]
|
||||||
|
|
||||||
|
NormalPath --> Leopard{色子点数是否为<br/>5 / 10 / 15 / 20 / 25 / 30?}
|
||||||
|
Leopard -->|否| NormalWin[五颗骰子点数和 = 该点数<br/>奖金 = 结算金额 × 底注]
|
||||||
|
Leopard -->|是| LeoRule{点数?}
|
||||||
|
LeoRule -->|5 或 30| MustBig[必定豹子大奖]
|
||||||
|
LeoRule -->|10 / 15 / 20 / 25| BigRate[按「奖励配置」页签「大奖权重」<br/>该点数权重决定真豹子或普通展示]
|
||||||
|
MustBig --> BigPay[豹子奖金 = 大奖结算金额 × 底注<br/>本局不再发该点数的普通奖]
|
||||||
|
BigRate -->|命中豹子| BigPay
|
||||||
|
BigRate -->|未中豹子| NonLeo[五颗骰子为非豹子组合<br/>奖金 = 结算金额 × 底注]
|
||||||
|
|
||||||
|
NormalWin --> T5Check
|
||||||
|
NonLeo --> T5Check
|
||||||
|
BigPay --> EndBig([本局结束:以豹子大奖为准])
|
||||||
|
DiceShow --> T5Check{档位为 T5 再来一次?}
|
||||||
|
T5Check -->|是| FreeTicket[赠送 1 次免费抽奖券<br/>下次免费局须相同底注]
|
||||||
|
T5Check -->|否| EndNormal([本局结束:以普通奖或惩罚为准])
|
||||||
|
FreeTicket --> EndNormal
|
||||||
|
|
||||||
|
style Start fill:#e8f4fc
|
||||||
|
style EndNormal fill:#e8fce8
|
||||||
|
style EndBig fill:#fff3e0</pre>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<div class="legend">
|
||||||
|
<h2>读图要点</h2>
|
||||||
|
<ul>
|
||||||
|
<li><strong>两步抽签</strong>:先抽档位 T1~T5(大奖 / 小赚 / 抽水 / 惩罚 / 再来一次),再在该档位 + 方向的多条奖励里按权重抽具体点数与结算金额。</li>
|
||||||
|
<li><strong>免费局与杀分局</strong>:都用杀分奖池的档位概率;一般不会出豹子大奖,也不会抽到只能组成豹子的点数 5、30。</li>
|
||||||
|
<li><strong>普通付费局</strong>:彩金池未到杀分条件时,用该玩家在「玩家管理」里的档位权重,才可能按「大奖权重」出豹子。</li>
|
||||||
|
<li><strong>玩家最终看到</strong>:色子点数、五颗骰子图案、到账平台币(普通奖 + 豹子奖)、是否获得「再来一次」免费券。</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
mermaid.initialize({
|
||||||
|
startOnLoad: true,
|
||||||
|
theme: 'neutral',
|
||||||
|
flowchart: { curve: 'basis', padding: 16, nodeSpacing: 28, rankSpacing: 40 }
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
44
server/docs/flowcharts/dice-为何抽到该奖励.mmd
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
flowchart TD
|
||||||
|
Start([玩家开始一局抽奖]) --> Dir[选择方向:顺时针 或 逆时针]
|
||||||
|
Dir --> Ante[选择底注倍数]
|
||||||
|
Ante --> Type{本局是否使用免费抽奖券?}
|
||||||
|
|
||||||
|
Type -->|是| Free[免费局]
|
||||||
|
Type -->|否且平台币足够| Paid[付费局:扣除底注对应平台币]
|
||||||
|
|
||||||
|
Free --> PoolKill[按「杀分奖池」的 T1~T5 档位概率抽签]
|
||||||
|
Paid --> KillCheck{彩金池已开启杀分<br/>且彩金池累计盈利 ≥ 安全线?}
|
||||||
|
KillCheck -->|是| PoolKill
|
||||||
|
KillCheck -->|否| PlayerW[按该玩家在「玩家管理」<br/>配置的 T1~T5 档位概率抽签]
|
||||||
|
|
||||||
|
PoolKill --> DrawTier[随机抽出档位 T1~T5]
|
||||||
|
PlayerW --> DrawTier
|
||||||
|
|
||||||
|
DrawTier --> PickRow[在「色子奖励权重」中<br/>取该档位 + 本局方向的所有行<br/>按行权重随机一条]
|
||||||
|
PickRow --> Got[得到:色子点数、结算金额、所属档位、落点格位]
|
||||||
|
|
||||||
|
Got --> KillMode{本局是否走杀分档位概率?}
|
||||||
|
KillMode -->|是| NoLeo[不发放豹子大奖<br/>且不会抽到仅能豹子的点数 5、30]
|
||||||
|
KillMode -->|否| NormalPath[按普通规则继续]
|
||||||
|
NoLeo --> DiceShow[生成五颗骰子并结算]
|
||||||
|
|
||||||
|
NormalPath --> Leopard{色子点数是否为<br/>5 / 10 / 15 / 20 / 25 / 30?}
|
||||||
|
Leopard -->|否| NormalWin[五颗骰子点数和 = 该点数<br/>奖金 = 结算金额 × 底注]
|
||||||
|
Leopard -->|是| LeoRule{点数?}
|
||||||
|
LeoRule -->|5 或 30| MustBig[必定豹子大奖]
|
||||||
|
LeoRule -->|10 / 15 / 20 / 25| BigRate[按「奖励配置」页签「大奖权重」<br/>该点数权重决定真豹子或普通展示]
|
||||||
|
MustBig --> BigPay[豹子奖金 = 大奖结算金额 × 底注<br/>本局不再发该点数的普通奖]
|
||||||
|
BigRate -->|命中豹子| BigPay
|
||||||
|
BigRate -->|未中豹子| NonLeo[五颗骰子为非豹子组合<br/>奖金 = 结算金额 × 底注]
|
||||||
|
|
||||||
|
NormalWin --> T5Check
|
||||||
|
NonLeo --> T5Check
|
||||||
|
BigPay --> EndBig([本局结束:以豹子大奖为准])
|
||||||
|
DiceShow --> T5Check{档位为 T5 再来一次?}
|
||||||
|
T5Check -->|是| FreeTicket[赠送 1 次免费抽奖券<br/>下次免费局须相同底注]
|
||||||
|
T5Check -->|否| EndNormal([本局结束:以普通奖或惩罚为准])
|
||||||
|
FreeTicket --> EndNormal
|
||||||
|
|
||||||
|
style Start fill:#e8f4fc
|
||||||
|
style EndNormal fill:#e8fce8
|
||||||
|
style EndBig fill:#fff3e0
|
||||||
219
server/docs/flowcharts/dice-后台中奖逻辑配置.html
Normal file
@@ -0,0 +1,219 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<title>后台如何配置中奖逻辑</title>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.min.js"></script>
|
||||||
|
<style>
|
||||||
|
* { box-sizing: border-box; }
|
||||||
|
body {
|
||||||
|
font-family: "Microsoft YaHei", "PingFang SC", sans-serif;
|
||||||
|
margin: 0;
|
||||||
|
padding: 24px 32px 48px;
|
||||||
|
background: #f0f4f8;
|
||||||
|
color: #1a1a2e;
|
||||||
|
line-height: 1.55;
|
||||||
|
}
|
||||||
|
header { max-width: 1200px; margin: 0 auto 16px; }
|
||||||
|
h1 { font-size: 1.45rem; margin: 0 0 6px; font-weight: 600; }
|
||||||
|
.subtitle { color: #5c6370; font-size: 0.92rem; margin: 0; }
|
||||||
|
.tip {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto 16px;
|
||||||
|
padding: 12px 16px;
|
||||||
|
background: #fff8e6;
|
||||||
|
border-left: 4px solid #e6a23c;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
.card {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto 20px;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 24px 20px;
|
||||||
|
box-shadow: 0 2px 12px rgba(0,0,0,.06);
|
||||||
|
}
|
||||||
|
.card h2 { font-size: 1.05rem; margin: 0 0 12px; color: #303133; }
|
||||||
|
.copy-hint {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto 12px;
|
||||||
|
font-size: 0.88rem;
|
||||||
|
color: #606266;
|
||||||
|
}
|
||||||
|
.copy-box {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 8px auto 20px;
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #dcdfe6;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 12px 14px;
|
||||||
|
}
|
||||||
|
.copy-box summary { cursor: pointer; font-size: 0.9rem; color: #409eff; }
|
||||||
|
.copy-box pre {
|
||||||
|
margin: 10px 0 0;
|
||||||
|
font-size: 0.76rem;
|
||||||
|
line-height: 1.4;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-all;
|
||||||
|
max-height: 320px;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
.steps {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
display: grid;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
.step {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 12px 16px;
|
||||||
|
border: 1px solid #e4e7ed;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
.step strong { color: #409eff; }
|
||||||
|
.mermaid { display: flex; justify-content: center; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header>
|
||||||
|
<h1>后台如何配置中奖逻辑</h1>
|
||||||
|
<p class="subtitle">按「一局真实抽奖」顺序:每个环节对应左侧菜单与页面按钮(与前台逻辑一致)</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<p class="tip">菜单根目录:<strong>大富翁-色子游戏</strong>。多渠道后台请先选顶部<strong>渠道</strong>,再改该渠道数据。</p>
|
||||||
|
<p class="copy-hint">复制:展开「Mermaid 源码」粘贴到流程图工具;日常维护也可打开同目录 <code>dice-后台中奖逻辑配置.mmd</code>。</p>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h2>主流程图(抽奖环节 → 去哪点哪个按钮)</h2>
|
||||||
|
<pre class="mermaid">
|
||||||
|
flowchart TD
|
||||||
|
O([按一局抽奖的真实顺序配置后台]) --> L1
|
||||||
|
|
||||||
|
L1[① 玩家选方向 + 底注] --> L1A[可选:大富翁-色子游戏 → 底注配置<br/>按钮:新增 / 行内编辑 → 提交]
|
||||||
|
L1A --> L2
|
||||||
|
|
||||||
|
L2[② 先随机抽出档位 T1~T5] --> L2Q{本局类型?}
|
||||||
|
L2Q -->|免费抽奖券| L2F[概率来源:杀分奖池 killScore]
|
||||||
|
L2Q -->|付费且彩金池杀分生效| L2F
|
||||||
|
L2Q -->|付费且未杀分| L2P[概率来源:该玩家档位权重]
|
||||||
|
|
||||||
|
L2F --> M2F[大富翁-色子游戏 → 彩金池配置<br/>按钮:行内「编辑」→ 名称 killScore<br/>填写 T1池权重~T5池权重 合计 100%<br/>按钮:「提交」]
|
||||||
|
L2P --> M2P[大富翁-色子游戏 → 玩家管理<br/>按钮:行内「编辑」<br/>填写 T1池权重~T5池权重 或 选择「彩金池配置」<br/>按钮:「提交」]
|
||||||
|
|
||||||
|
M2F --> L2K
|
||||||
|
M2P --> L2K
|
||||||
|
L2K[杀分何时对付费局生效] --> M2K[大富翁-色子游戏 → 彩金池配置<br/>按钮:「查看当前彩金池」<br/>填写「安全线」· 开关「开启杀分」<br/>按钮:「保存安全线」]
|
||||||
|
|
||||||
|
M2K --> L3
|
||||||
|
L3[③ 在档位内随机一条奖励行] --> M3A[须先有盘面金额与档位规则]
|
||||||
|
M3A --> M3B[大富翁-色子游戏 → 奖励配置<br/>页签「奖励索引」→ 填写结算金额等<br/>按钮:「保存」]
|
||||||
|
M3B --> M3C[奖励配置 → 按钮「创建奖励对照」<br/>弹窗 → 按钮「确认导入」]
|
||||||
|
M3C --> M3D[大富翁-色子游戏 → 色子奖励权重<br/>按钮:「权重配比」→ 页签顺时针/逆时针<br/>按 T1~T5 填各点数权重 → 按钮「提交」]
|
||||||
|
|
||||||
|
M3D --> L4
|
||||||
|
L4[④ 若抽到豹子点数 5/10/15/20/25/30] --> L4Q{本局是否杀分档位?}
|
||||||
|
L4Q -->|是| L4N[不触发豹子大奖]
|
||||||
|
L4Q -->|否| L4Y[可能触发豹子大奖]
|
||||||
|
L4Y --> M4[大富翁-色子游戏 → 奖励配置<br/>页签「大奖权重」→ 拖动权重滑条<br/>按钮:「保存」<br/>说明:点数 5、30 固定必中;10/15/20/25 可调]
|
||||||
|
|
||||||
|
L4N --> L5
|
||||||
|
M4 --> L5
|
||||||
|
L5[⑤ 验证后上线] --> M5A[色子奖励权重 → 按钮「一键测试权重」<br/>弹窗 → 按钮「开始测试」]
|
||||||
|
M5A --> M5B[权重测试记录 → 按钮「查看详情」<br/>按钮「导入到当前配置」→「确认导入」]
|
||||||
|
M5B --> Done([可对玩家开放;用「玩家抽奖记录」核对])
|
||||||
|
|
||||||
|
style O fill:#e8f4fc
|
||||||
|
style Done fill:#e8fce8
|
||||||
|
style M2F fill:#fdf6ec
|
||||||
|
style M2P fill:#fdf6ec
|
||||||
|
style M3D fill:#fde2e2
|
||||||
|
style M4 fill:#e1f3d8
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<details class="copy-box">
|
||||||
|
<summary>Mermaid 源码(可复制,与同目录 .mmd 文件一致)</summary>
|
||||||
|
<pre id="mermaid-src">flowchart TD
|
||||||
|
O([按一局抽奖的真实顺序配置后台]) --> L1
|
||||||
|
|
||||||
|
L1[① 玩家选方向 + 底注] --> L1A[可选:大富翁-色子游戏 → 底注配置<br/>按钮:新增 / 行内编辑 → 提交]
|
||||||
|
L1A --> L2
|
||||||
|
|
||||||
|
L2[② 先随机抽出档位 T1~T5] --> L2Q{本局类型?}
|
||||||
|
L2Q -->|免费抽奖券| L2F[概率来源:杀分奖池 killScore]
|
||||||
|
L2Q -->|付费且彩金池杀分生效| L2F
|
||||||
|
L2Q -->|付费且未杀分| L2P[概率来源:该玩家档位权重]
|
||||||
|
|
||||||
|
L2F --> M2F[大富翁-色子游戏 → 彩金池配置<br/>按钮:行内「编辑」→ 名称 killScore<br/>填写 T1池权重~T5池权重 合计 100%<br/>按钮:「提交」]
|
||||||
|
L2P --> M2P[大富翁-色子游戏 → 玩家管理<br/>按钮:行内「编辑」<br/>填写 T1池权重~T5池权重 或 选择「彩金池配置」<br/>按钮:「提交」]
|
||||||
|
|
||||||
|
M2F --> L2K
|
||||||
|
M2P --> L2K
|
||||||
|
L2K[杀分何时对付费局生效] --> M2K[大富翁-色子游戏 → 彩金池配置<br/>按钮:「查看当前彩金池」<br/>填写「安全线」· 开关「开启杀分」<br/>按钮:「保存安全线」]
|
||||||
|
|
||||||
|
M2K --> L3
|
||||||
|
L3[③ 在档位内随机一条奖励行] --> M3A[须先有盘面金额与档位规则]
|
||||||
|
M3A --> M3B[大富翁-色子游戏 → 奖励配置<br/>页签「奖励索引」→ 填写结算金额等<br/>按钮:「保存」]
|
||||||
|
M3B --> M3C[奖励配置 → 按钮「创建奖励对照」<br/>弹窗 → 按钮「确认导入」]
|
||||||
|
M3C --> M3D[大富翁-色子游戏 → 色子奖励权重<br/>按钮:「权重配比」→ 页签顺时针/逆时针<br/>按 T1~T5 填各点数权重 → 按钮「提交」]
|
||||||
|
|
||||||
|
M3D --> L4
|
||||||
|
L4[④ 若抽到豹子点数 5/10/15/20/25/30] --> L4Q{本局是否杀分档位?}
|
||||||
|
L4Q -->|是| L4N[不触发豹子大奖]
|
||||||
|
L4Q -->|否| L4Y[可能触发豹子大奖]
|
||||||
|
L4Y --> M4[大富翁-色子游戏 → 奖励配置<br/>页签「大奖权重」→ 拖动权重滑条<br/>按钮:「保存」<br/>说明:点数 5、30 固定必中;10/15/20/25 可调]
|
||||||
|
|
||||||
|
L4N --> L5
|
||||||
|
M4 --> L5
|
||||||
|
L5[⑤ 验证后上线] --> M5A[色子奖励权重 → 按钮「一键测试权重」<br/>弹窗 → 按钮「开始测试」]
|
||||||
|
M5A --> M5B[权重测试记录 → 按钮「查看详情」<br/>按钮「导入到当前配置」→「确认导入」]
|
||||||
|
M5B --> Done([可对玩家开放;用「玩家抽奖记录」核对])
|
||||||
|
|
||||||
|
style O fill:#e8f4fc
|
||||||
|
style Done fill:#e8fce8
|
||||||
|
style M2F fill:#fdf6ec
|
||||||
|
style M2P fill:#fdf6ec
|
||||||
|
style M3D fill:#fde2e2
|
||||||
|
style M4 fill:#e1f3d8</pre>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h2>首次搭建推荐顺序(与上图环节对应)</h2>
|
||||||
|
<pre class="mermaid">
|
||||||
|
flowchart TD
|
||||||
|
O([开始配置]) --> R1[奖励配置 · 页签「奖励索引」· 按钮「保存」]
|
||||||
|
R1 --> R2[奖励配置 · 页签「大奖权重」· 按钮「保存」]
|
||||||
|
R2 --> R3[奖励配置 · 按钮「创建奖励对照」·「确认导入」]
|
||||||
|
R3 --> W[色子奖励权重 · 按钮「权重配比」· 按钮「提交」]
|
||||||
|
W --> P1[彩金池配置 · 行内「编辑」default / killScore ·「提交」]
|
||||||
|
P1 --> P2[彩金池配置 ·「查看当前彩金池」·「保存安全线」]
|
||||||
|
P2 --> PL[玩家管理 · 行内「编辑」· 档位权重 ·「提交」]
|
||||||
|
PL --> T{要仿真?}
|
||||||
|
T -->|是| Test[色子奖励权重 ·「一键测试权重」·「开始测试」]
|
||||||
|
Test --> Imp[权重测试记录 ·「查看详情」·「导入到当前配置」·「确认导入」]
|
||||||
|
T -->|否| Live([上线])
|
||||||
|
Imp --> Live
|
||||||
|
|
||||||
|
style O fill:#e8f4fc
|
||||||
|
style Live fill:#e8fce8
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="steps">
|
||||||
|
<div class="step"><strong>档位含义</strong>:T1 大奖 · T2 小赚 · T3 抽水 · T4 惩罚 · T5 再来一次(由「奖励索引」结算金额规则决定,见页内说明)。</div>
|
||||||
|
<div class="step"><strong>改「奖励索引」后</strong>:必须再点「创建奖励对照」→「确认导入」,否则抽奖仍用旧对照表。</div>
|
||||||
|
<div class="step"><strong>核对真实对局</strong>:大富翁-色子游戏 → 玩家抽奖记录(看奖励档位、色子点数、摇色子中奖平台币、中大奖平台币、底注、方向)。</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
mermaid.initialize({
|
||||||
|
startOnLoad: true,
|
||||||
|
theme: 'neutral',
|
||||||
|
flowchart: { curve: 'basis', padding: 14, nodeSpacing: 24, rankSpacing: 36 }
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
42
server/docs/flowcharts/dice-后台中奖逻辑配置.mmd
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
flowchart TD
|
||||||
|
O([按一局抽奖的真实顺序配置后台]) --> L1
|
||||||
|
|
||||||
|
L1[① 玩家选方向 + 底注] --> L1A[可选:大富翁-色子游戏 → 底注配置<br/>按钮:新增 / 行内编辑 → 提交]
|
||||||
|
L1A --> L2
|
||||||
|
|
||||||
|
L2[② 先随机抽出档位 T1~T5] --> L2Q{本局类型?}
|
||||||
|
L2Q -->|免费抽奖券| L2F[概率来源:杀分奖池 killScore]
|
||||||
|
L2Q -->|付费且彩金池杀分生效| L2F
|
||||||
|
L2Q -->|付费且未杀分| L2P[概率来源:该玩家档位权重]
|
||||||
|
|
||||||
|
L2F --> M2F[大富翁-色子游戏 → 彩金池配置<br/>按钮:行内「编辑」→ 名称 killScore<br/>填写 T1池权重~T5池权重 合计 100%<br/>按钮:「提交」]
|
||||||
|
L2P --> M2P[大富翁-色子游戏 → 玩家管理<br/>按钮:行内「编辑」<br/>填写 T1池权重~T5池权重 或 选择「彩金池配置」<br/>按钮:「提交」]
|
||||||
|
|
||||||
|
M2F --> L2K
|
||||||
|
M2P --> L2K
|
||||||
|
L2K[杀分何时对付费局生效] --> M2K[大富翁-色子游戏 → 彩金池配置<br/>按钮:「查看当前彩金池」<br/>填写「安全线」· 开关「开启杀分」<br/>按钮:「保存安全线」]
|
||||||
|
|
||||||
|
M2K --> L3
|
||||||
|
L3[③ 在档位内随机一条奖励行] --> M3A[须先有盘面金额与档位规则]
|
||||||
|
M3A --> M3B[大富翁-色子游戏 → 奖励配置<br/>页签「奖励索引」→ 填写结算金额等<br/>按钮:「保存」]
|
||||||
|
M3B --> M3C[奖励配置 → 按钮「创建奖励对照」<br/>弹窗 → 按钮「确认导入」]
|
||||||
|
M3C --> M3D[大富翁-色子游戏 → 色子奖励权重<br/>按钮:「权重配比」→ 页签顺时针/逆时针<br/>按 T1~T5 填各点数权重 → 按钮「提交」]
|
||||||
|
|
||||||
|
M3D --> L4
|
||||||
|
L4[④ 若抽到豹子点数 5/10/15/20/25/30] --> L4Q{本局是否杀分档位?}
|
||||||
|
L4Q -->|是| L4N[不触发豹子大奖]
|
||||||
|
L4Q -->|否| L4Y[可能触发豹子大奖]
|
||||||
|
L4Y --> M4[大富翁-色子游戏 → 奖励配置<br/>页签「大奖权重」→ 拖动权重滑条<br/>按钮:「保存」<br/>说明:点数 5、30 固定必中;10/15/20/25 可调]
|
||||||
|
|
||||||
|
L4N --> L5
|
||||||
|
M4 --> L5
|
||||||
|
L5[⑤ 验证后上线] --> M5A[色子奖励权重 → 按钮「一键测试权重」<br/>弹窗 → 按钮「开始测试」]
|
||||||
|
M5A --> M5B[权重测试记录 → 按钮「查看详情」<br/>按钮「导入到当前配置」→「确认导入」]
|
||||||
|
M5B --> Done([可对玩家开放;用「玩家抽奖记录」核对])
|
||||||
|
|
||||||
|
style O fill:#e8f4fc
|
||||||
|
style Done fill:#e8fce8
|
||||||
|
style M2F fill:#fdf6ec
|
||||||
|
style M2P fill:#fdf6ec
|
||||||
|
style M3D fill:#fde2e2
|
||||||
|
style M4 fill:#e1f3d8
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
<?php
|
||||||
|
// +----------------------------------------------------------------------
|
||||||
|
// | saiadmin [ saiadmin快速开发框架 ]
|
||||||
|
// +----------------------------------------------------------------------
|
||||||
|
namespace plugin\saiadmin\app\controller\system;
|
||||||
|
|
||||||
|
use plugin\saiadmin\app\logic\system\SystemAdminGuideLogic;
|
||||||
|
use plugin\saiadmin\basic\BaseController;
|
||||||
|
use plugin\saiadmin\service\Permission;
|
||||||
|
use support\Request;
|
||||||
|
use support\Response;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 后台操作指南控制器
|
||||||
|
*/
|
||||||
|
class SystemAdminGuideController extends BaseController
|
||||||
|
{
|
||||||
|
private SystemAdminGuideLogic $guideLogic;
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->guideLogic = new SystemAdminGuideLogic();
|
||||||
|
parent::__construct();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 读取后台操作指南 Markdown 内容
|
||||||
|
*/
|
||||||
|
#[Permission('后台操作指南读取', 'system:admin_guide:index:read')]
|
||||||
|
public function read(Request $request): Response
|
||||||
|
{
|
||||||
|
$data = $this->guideLogic->read();
|
||||||
|
return $this->success($data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存后台操作指南 Markdown 内容
|
||||||
|
*/
|
||||||
|
#[Permission('后台操作指南保存', 'system:admin_guide:index:save')]
|
||||||
|
public function save(Request $request): Response
|
||||||
|
{
|
||||||
|
$content = $request->post('content', '');
|
||||||
|
if (! is_string($content)) {
|
||||||
|
return $this->fail('invalid content');
|
||||||
|
}
|
||||||
|
$data = $this->guideLogic->save($content);
|
||||||
|
return $this->success($data, 'save success');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -107,11 +107,12 @@ class SystemDeptController extends BaseController
|
|||||||
#[Permission('渠道数据删除','core:dept:destroy')]
|
#[Permission('渠道数据删除','core:dept:destroy')]
|
||||||
public function destroy(Request $request) : Response
|
public function destroy(Request $request) : Response
|
||||||
{
|
{
|
||||||
$ids = $request->post('ids', '');
|
// DELETE + JSON body 须用 input();post() 仅表单 POST 有效(与 SystemUserController 一致)
|
||||||
|
$ids = $request->input('ids', '');
|
||||||
if (empty($ids)) {
|
if (empty($ids)) {
|
||||||
return $this->fail('please select data to delete');
|
return $this->fail('please select data to delete');
|
||||||
}
|
}
|
||||||
$deleteTables = $request->post('delete_tables', []);
|
$deleteTables = $request->input('delete_tables', []);
|
||||||
if (!is_array($deleteTables)) {
|
if (!is_array($deleteTables)) {
|
||||||
$deleteTables = [];
|
$deleteTables = [];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ class SystemLogController extends BaseController
|
|||||||
['username', ''],
|
['username', ''],
|
||||||
['status', ''],
|
['status', ''],
|
||||||
['ip', ''],
|
['ip', ''],
|
||||||
|
['dept_id', ''],
|
||||||
]);
|
]);
|
||||||
$logic = new SystemLoginLogLogic();
|
$logic = new SystemLoginLogLogic();
|
||||||
$query = $logic->search($where);
|
$query = $logic->search($where);
|
||||||
@@ -71,6 +72,7 @@ class SystemLogController extends BaseController
|
|||||||
['service_name', ''],
|
['service_name', ''],
|
||||||
['router', ''],
|
['router', ''],
|
||||||
['ip', ''],
|
['ip', ''],
|
||||||
|
['dept_id', ''],
|
||||||
]);
|
]);
|
||||||
$logic = new SystemOperLogLogic();
|
$logic = new SystemOperLogLogic();
|
||||||
$logic->init($this->adminInfo);
|
$logic->init($this->adminInfo);
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ namespace plugin\saiadmin\app\event;
|
|||||||
use plugin\saiadmin\app\cache\ReflectionCache;
|
use plugin\saiadmin\app\cache\ReflectionCache;
|
||||||
use plugin\saiadmin\app\model\system\SystemLoginLog;
|
use plugin\saiadmin\app\model\system\SystemLoginLog;
|
||||||
use plugin\saiadmin\app\model\system\SystemOperLog;
|
use plugin\saiadmin\app\model\system\SystemOperLog;
|
||||||
|
use plugin\saiadmin\app\model\system\SystemUser as SystemUserModel;
|
||||||
|
|
||||||
class SystemUser
|
class SystemUser
|
||||||
{
|
{
|
||||||
@@ -32,6 +33,10 @@ class SystemUser
|
|||||||
if (isset($item['admin_id'])) {
|
if (isset($item['admin_id'])) {
|
||||||
$data['created_by'] = $item['admin_id'];
|
$data['created_by'] = $item['admin_id'];
|
||||||
$data['updated_by'] = $item['admin_id'];
|
$data['updated_by'] = $item['admin_id'];
|
||||||
|
$deptId = SystemUserModel::where('id', $item['admin_id'])->value('dept_id');
|
||||||
|
if ($deptId !== null && $deptId !== '' && $deptId > 0) {
|
||||||
|
$data['dept_id'] = $deptId;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
SystemLoginLog::create($data);
|
SystemLoginLog::create($data);
|
||||||
}
|
}
|
||||||
@@ -49,6 +54,9 @@ class SystemUser
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
$info = getCurrentInfo();
|
$info = getCurrentInfo();
|
||||||
|
if (!$info) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
$ip = $request->getRealIp();
|
$ip = $request->getRealIp();
|
||||||
$module = $request->plugin;
|
$module = $request->plugin;
|
||||||
$rule = trim($request->uri());
|
$rule = trim($request->uri());
|
||||||
@@ -60,6 +68,14 @@ class SystemUser
|
|||||||
$data['ip'] = $ip;
|
$data['ip'] = $ip;
|
||||||
$data['ip_location'] = self::getIpLocation($ip);
|
$data['ip_location'] = self::getIpLocation($ip);
|
||||||
$data['request_data'] = $this->filterParams($request->all());
|
$data['request_data'] = $this->filterParams($request->all());
|
||||||
|
if (isset($info['dept_id']) && $info['dept_id'] > 0) {
|
||||||
|
$data['dept_id'] = $info['dept_id'];
|
||||||
|
} elseif (isset($info['id'])) {
|
||||||
|
$deptId = SystemUserModel::where('id', $info['id'])->value('dept_id');
|
||||||
|
if ($deptId !== null && $deptId !== '' && $deptId > 0) {
|
||||||
|
$data['dept_id'] = $deptId;
|
||||||
|
}
|
||||||
|
}
|
||||||
SystemOperLog::create($data);
|
SystemOperLog::create($data);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,72 @@
|
|||||||
|
<?php
|
||||||
|
// +----------------------------------------------------------------------
|
||||||
|
// | saiadmin [ saiadmin快速开发框架 ]
|
||||||
|
// +----------------------------------------------------------------------
|
||||||
|
namespace plugin\saiadmin\app\logic\system;
|
||||||
|
|
||||||
|
use plugin\saiadmin\exception\ApiException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 后台操作指南逻辑(读写 server/docs/ADMIN_GUIDE.md)
|
||||||
|
*/
|
||||||
|
class SystemAdminGuideLogic
|
||||||
|
{
|
||||||
|
private const GUIDE_FILENAME = 'ADMIN_GUIDE.md';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取指南 Markdown 文件绝对路径
|
||||||
|
*/
|
||||||
|
public function getFilePath(): string
|
||||||
|
{
|
||||||
|
return base_path() . DIRECTORY_SEPARATOR . 'docs' . DIRECTORY_SEPARATOR . self::GUIDE_FILENAME;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 读取指南内容
|
||||||
|
* @return array{content: string, file_path: string, update_time: string|null}
|
||||||
|
*/
|
||||||
|
public function read(): array
|
||||||
|
{
|
||||||
|
$filePath = $this->getFilePath();
|
||||||
|
if (! is_file($filePath)) {
|
||||||
|
throw new ApiException('admin guide file not found');
|
||||||
|
}
|
||||||
|
$content = file_get_contents($filePath);
|
||||||
|
if ($content === false) {
|
||||||
|
throw new ApiException('failed to read admin guide file');
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'content' => $content,
|
||||||
|
'file_path' => 'docs/' . self::GUIDE_FILENAME,
|
||||||
|
'update_time' => date('Y-m-d H:i:s', filemtime($filePath)),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存指南内容到 Markdown 文件
|
||||||
|
* @param string $content
|
||||||
|
* @return array{content: string, file_path: string, update_time: string}
|
||||||
|
*/
|
||||||
|
public function save(string $content): array
|
||||||
|
{
|
||||||
|
$filePath = $this->getFilePath();
|
||||||
|
$dir = dirname($filePath);
|
||||||
|
if (! is_dir($dir) && ! mkdir($dir, 0755, true) && ! is_dir($dir)) {
|
||||||
|
throw new ApiException('failed to create docs directory');
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = file_put_contents($filePath, $content, LOCK_EX);
|
||||||
|
if ($result === false) {
|
||||||
|
throw new ApiException('failed to save admin guide file');
|
||||||
|
}
|
||||||
|
|
||||||
|
clearstatcache(true, $filePath);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'content' => $content,
|
||||||
|
'file_path' => 'docs/' . self::GUIDE_FILENAME,
|
||||||
|
'update_time' => date('Y-m-d H:i:s', filemtime($filePath)),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -20,6 +20,10 @@ use support\think\Db;
|
|||||||
*/
|
*/
|
||||||
class SystemRoleLogic extends BaseLogic
|
class SystemRoleLogic extends BaseLogic
|
||||||
{
|
{
|
||||||
|
protected string $orderField = 'level';
|
||||||
|
|
||||||
|
protected string $orderType = 'desc';
|
||||||
|
|
||||||
public function __construct()
|
public function __construct()
|
||||||
{
|
{
|
||||||
$this->model = new SystemRole();
|
$this->model = new SystemRole();
|
||||||
@@ -110,7 +114,6 @@ class SystemRoleLogic extends BaseLogic
|
|||||||
$query->where('level', '<', $maxLevel);
|
$query->where('level', '<', $maxLevel);
|
||||||
}
|
}
|
||||||
$query->where('id', '<>', SystemRoleChannelService::SUPER_ADMIN_ROLE_ID);
|
$query->where('id', '<>', SystemRoleChannelService::SUPER_ADMIN_ROLE_ID);
|
||||||
$query->order('sort', 'desc');
|
|
||||||
return $this->getAll($query);
|
return $this->getAll($query);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ use plugin\saiadmin\basic\think\BaseModel;
|
|||||||
* @property $status 登录状态
|
* @property $status 登录状态
|
||||||
* @property $message 提示消息
|
* @property $message 提示消息
|
||||||
* @property $login_time 登录时间
|
* @property $login_time 登录时间
|
||||||
|
* @property $dept_id 所属渠道
|
||||||
* @property $remark 备注
|
* @property $remark 备注
|
||||||
* @property $created_by 创建者
|
* @property $created_by 创建者
|
||||||
* @property $updated_by 更新者
|
* @property $updated_by 更新者
|
||||||
@@ -47,4 +48,14 @@ class SystemLoginLog extends BaseModel
|
|||||||
$query->whereTime('login_time', 'between', $value);
|
$query->whereTime('login_time', 'between', $value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 渠道搜索
|
||||||
|
*/
|
||||||
|
public function searchDeptIdAttr($query, $value): void
|
||||||
|
{
|
||||||
|
if ($value !== '' && $value !== null && $value > 0) {
|
||||||
|
$query->where('dept_id', '=', $value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -23,6 +23,7 @@ use plugin\saiadmin\basic\think\BaseModel;
|
|||||||
* @property $ip 请求IP地址
|
* @property $ip 请求IP地址
|
||||||
* @property $ip_location IP所属地
|
* @property $ip_location IP所属地
|
||||||
* @property $request_data 请求数据
|
* @property $request_data 请求数据
|
||||||
|
* @property $dept_id 所属渠道
|
||||||
* @property $remark 备注
|
* @property $remark 备注
|
||||||
* @property $created_by 创建者
|
* @property $created_by 创建者
|
||||||
* @property $updated_by 更新者
|
* @property $updated_by 更新者
|
||||||
@@ -39,4 +40,14 @@ class SystemOperLog extends BaseModel
|
|||||||
|
|
||||||
protected $table = 'sa_system_oper_log';
|
protected $table = 'sa_system_oper_log';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 渠道搜索
|
||||||
|
*/
|
||||||
|
public function searchDeptIdAttr($query, $value): void
|
||||||
|
{
|
||||||
|
if ($value !== '' && $value !== null && $value > 0) {
|
||||||
|
$query->where('dept_id', '=', $value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -189,12 +189,16 @@ class SystemRoleChannelService
|
|||||||
if ($this->tableHasColumn('sa_system_role', 'dept_id')) {
|
if ($this->tableHasColumn('sa_system_role', 'dept_id')) {
|
||||||
$query->where('dept_id', 0);
|
$query->where('dept_id', 0);
|
||||||
}
|
}
|
||||||
$rows = $query->order('sort', 'desc')->select()->toArray();
|
$rows = $query->select()->toArray();
|
||||||
if (count($rows) === count($codes)) {
|
if (empty($rows)) {
|
||||||
return $rows;
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
// 按配置顺序返回,缺失的 code 跳过
|
if (count($rows) === count($codes)) {
|
||||||
|
return $this->sortRolesByLevelDesc($rows);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 按配置 code 补齐,缺失的 code 跳过,最终按角色级别从大到小排序
|
||||||
$byCode = [];
|
$byCode = [];
|
||||||
foreach ($rows as $row) {
|
foreach ($rows as $row) {
|
||||||
$byCode[(string) ($row['code'] ?? '')] = $row;
|
$byCode[(string) ($row['code'] ?? '')] = $row;
|
||||||
@@ -205,7 +209,25 @@ class SystemRoleChannelService
|
|||||||
$ordered[] = $byCode[$code];
|
$ordered[] = $byCode[$code];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return $ordered;
|
return $this->sortRolesByLevelDesc($ordered);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按角色级别从大到小排序(同级别按 sort 降序)
|
||||||
|
*
|
||||||
|
* @param array<int, array<string, mixed>> $rows
|
||||||
|
* @return array<int, array<string, mixed>>
|
||||||
|
*/
|
||||||
|
private function sortRolesByLevelDesc(array $rows): array
|
||||||
|
{
|
||||||
|
usort($rows, static function (array $a, array $b): int {
|
||||||
|
$levelCompare = (int) ($b['level'] ?? 0) <=> (int) ($a['level'] ?? 0);
|
||||||
|
if ($levelCompare !== 0) {
|
||||||
|
return $levelCompare;
|
||||||
|
}
|
||||||
|
return (int) ($b['sort'] ?? 0) <=> (int) ($a['sort'] ?? 0);
|
||||||
|
});
|
||||||
|
return $rows;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function insertRoleFromTemplate(array $template, int $deptId): int
|
private function insertRoleFromTemplate(array $template, int $deptId): int
|
||||||
|
|||||||
@@ -12,10 +12,18 @@ use plugin\saiadmin\basic\contracts\ModelInterface;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* ThinkORM 模型基类
|
* ThinkORM 模型基类
|
||||||
|
*
|
||||||
|
* 全局策略:所有删除一律为硬删除(物理删除)。
|
||||||
|
* - 保留 SoftDelete trait 仅是为了兼容历史字段(如 delete_time)与查询作用域,
|
||||||
|
* 实际删除方法(delete/destroy)均通过 trait 别名重写为强制 force=true。
|
||||||
|
* - 项目中不使用 withTrashed/onlyTrashed/restore() 等软删除恢复接口。
|
||||||
*/
|
*/
|
||||||
class BaseModel extends Model implements ModelInterface
|
class BaseModel extends Model implements ModelInterface
|
||||||
{
|
{
|
||||||
use SoftDelete;
|
use SoftDelete {
|
||||||
|
delete as protected softDeleteCascadeOriginal;
|
||||||
|
destroy as protected softDeleteDestroyOriginal;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 删除时间字段
|
* 删除时间字段
|
||||||
@@ -99,6 +107,25 @@ class BaseModel extends Model implements ModelInterface
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除记录(静态入口):一律强制硬删除(物理删除)。
|
||||||
|
* @param mixed $data 主键、闭包或条件
|
||||||
|
* @param bool $force 兼容签名,内部一律按 true 处理
|
||||||
|
*/
|
||||||
|
public static function destroy($data, bool $force = true): bool
|
||||||
|
{
|
||||||
|
return static::softDeleteDestroyOriginal($data, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除记录(实例方法):一律强制硬删除。
|
||||||
|
*/
|
||||||
|
public function delete(): bool
|
||||||
|
{
|
||||||
|
$this->force(true);
|
||||||
|
return $this->softDeleteCascadeOriginal();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 新增前事件:自动写入 create_time,有后台登录信息时写入 created_by
|
* 新增前事件:自动写入 create_time,有后台登录信息时写入 created_by
|
||||||
* @param Model $model
|
* @param Model $model
|
||||||
|
|||||||
@@ -81,6 +81,10 @@ Route::group('/core', function () {
|
|||||||
Route::delete("/logs/deleteOperLog", [\plugin\saiadmin\app\controller\system\SystemLogController::class, 'deleteOperLog']);
|
Route::delete("/logs/deleteOperLog", [\plugin\saiadmin\app\controller\system\SystemLogController::class, 'deleteOperLog']);
|
||||||
fastRoute("email", \plugin\saiadmin\app\controller\system\SystemMailController::class);
|
fastRoute("email", \plugin\saiadmin\app\controller\system\SystemMailController::class);
|
||||||
|
|
||||||
|
// 后台操作指南
|
||||||
|
Route::get("/adminGuide/read", [\plugin\saiadmin\app\controller\system\SystemAdminGuideController::class, 'read']);
|
||||||
|
Route::post("/adminGuide/save", [\plugin\saiadmin\app\controller\system\SystemAdminGuideController::class, 'save']);
|
||||||
|
|
||||||
// 服务管理
|
// 服务管理
|
||||||
Route::get("/server/monitor", [\plugin\saiadmin\app\controller\system\SystemServerController::class, 'monitor']);
|
Route::get("/server/monitor", [\plugin\saiadmin\app\controller\system\SystemServerController::class, 'monitor']);
|
||||||
Route::get("/server/cache", [\plugin\saiadmin\app\controller\system\SystemServerController::class, 'cache']);
|
Route::get("/server/cache", [\plugin\saiadmin\app\controller\system\SystemServerController::class, 'cache']);
|
||||||
@@ -115,7 +119,9 @@ Route::group('/core', function () {
|
|||||||
Route::post('/dice/reward_config/DiceRewardConfig/batchUpdateWeights', [\app\dice\controller\reward_config\DiceRewardConfigController::class, 'batchUpdateWeights']);
|
Route::post('/dice/reward_config/DiceRewardConfig/batchUpdateWeights', [\app\dice\controller\reward_config\DiceRewardConfigController::class, 'batchUpdateWeights']);
|
||||||
Route::post('/dice/reward_config/DiceRewardConfig/saveBigwinWeightsByGrid', [\app\dice\controller\reward_config\DiceRewardConfigController::class, 'saveBigwinWeightsByGrid']);
|
Route::post('/dice/reward_config/DiceRewardConfig/saveBigwinWeightsByGrid', [\app\dice\controller\reward_config\DiceRewardConfigController::class, 'saveBigwinWeightsByGrid']);
|
||||||
Route::post('/dice/reward_config/DiceRewardConfig/batchUpdate', [\app\dice\controller\reward_config\DiceRewardConfigController::class, 'batchUpdate']);
|
Route::post('/dice/reward_config/DiceRewardConfig/batchUpdate', [\app\dice\controller\reward_config\DiceRewardConfigController::class, 'batchUpdate']);
|
||||||
|
Route::post('/dice/reward_config/DiceRewardConfig/generateIndexByRules', [\app\dice\controller\reward_config\DiceRewardConfigController::class, 'generateIndexByRules']);
|
||||||
Route::post('/dice/reward_config/DiceRewardConfig/createRewardReference', [\app\dice\controller\reward_config\DiceRewardConfigController::class, 'createRewardReference']);
|
Route::post('/dice/reward_config/DiceRewardConfig/createRewardReference', [\app\dice\controller\reward_config\DiceRewardConfigController::class, 'createRewardReference']);
|
||||||
|
Route::post('/dice/reward_config/DiceRewardConfig/createRewardReferencePreview', [\app\dice\controller\reward_config\DiceRewardConfigController::class, 'createRewardReferencePreview']);
|
||||||
Route::post('/dice/reward_config/DiceRewardConfig/runWeightTest', [\app\dice\controller\reward_config\DiceRewardConfigController::class, 'runWeightTest']);
|
Route::post('/dice/reward_config/DiceRewardConfig/runWeightTest', [\app\dice\controller\reward_config\DiceRewardConfigController::class, 'runWeightTest']);
|
||||||
fastRoute('dice/game/DiceGame', \app\dice\controller\game\DiceGameController::class);
|
fastRoute('dice/game/DiceGame', \app\dice\controller\game\DiceGameController::class);
|
||||||
fastRoute('dice/ante_config/DiceAnteConfig', \app\dice\controller\ante_config\DiceAnteConfigController::class);
|
fastRoute('dice/ante_config/DiceAnteConfig', \app\dice\controller\ante_config\DiceAnteConfigController::class);
|
||||||
|
|||||||
BIN
server/public/docs/picture/guide_01.png
Normal file
|
After Width: | Height: | Size: 122 KiB |
BIN
server/public/docs/picture/guide_02.png
Normal file
|
After Width: | Height: | Size: 92 KiB |
BIN
server/public/docs/picture/guide_03.png
Normal file
|
After Width: | Height: | Size: 156 KiB |
BIN
server/public/docs/picture/guide_04.png
Normal file
|
After Width: | Height: | Size: 141 KiB |
BIN
server/public/docs/picture/guide_05.png
Normal file
|
After Width: | Height: | Size: 166 KiB |
BIN
server/public/docs/picture/guide_06.png
Normal file
|
After Width: | Height: | Size: 671 KiB |
BIN
server/public/docs/picture/guide_07.png
Normal file
|
After Width: | Height: | Size: 121 KiB |
BIN
server/public/docs/picture/guide_08.png
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
server/public/docs/picture/guide_09.png
Normal file
|
After Width: | Height: | Size: 153 KiB |
BIN
server/public/docs/picture/guide_10.png
Normal file
|
After Width: | Height: | Size: 92 KiB |
BIN
server/public/docs/picture/guide_11.png
Normal file
|
After Width: | Height: | Size: 141 KiB |
BIN
server/public/docs/picture/guide_12.png
Normal file
|
After Width: | Height: | Size: 92 KiB |
BIN
server/public/docs/picture/guide_13.png
Normal file
|
After Width: | Height: | Size: 160 KiB |
BIN
server/public/docs/picture/guide_14.png
Normal file
|
After Width: | Height: | Size: 134 KiB |
BIN
server/public/docs/picture/guide_15.png
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
BIN
server/public/docs/picture/guide_16.png
Normal file
|
After Width: | Height: | Size: 146 KiB |
BIN
server/public/docs/picture/guide_17.png
Normal file
|
After Width: | Height: | Size: 183 KiB |
BIN
server/public/docs/picture/guide_18.png
Normal file
|
After Width: | Height: | Size: 177 KiB |
BIN
server/public/docs/picture/guide_19.png
Normal file
|
After Width: | Height: | Size: 110 KiB |
BIN
server/public/docs/picture/guide_20.png
Normal file
|
After Width: | Height: | Size: 157 KiB |
BIN
server/public/docs/picture/guide_21.png
Normal file
|
After Width: | Height: | Size: 125 KiB |
BIN
server/public/docs/picture/guide_22.png
Normal file
|
After Width: | Height: | Size: 112 KiB |
BIN
server/public/docs/picture/guide_23.png
Normal file
|
After Width: | Height: | Size: 145 KiB |
BIN
server/public/docs/picture/guide_24.png
Normal file
|
After Width: | Height: | Size: 104 KiB |
BIN
server/public/docs/picture/guide_25.png
Normal file
|
After Width: | Height: | Size: 161 KiB |
BIN
server/public/docs/picture/guide_26.png
Normal file
|
After Width: | Height: | Size: 113 KiB |
@@ -4,6 +4,9 @@ declare(strict_types=1);
|
|||||||
return [
|
return [
|
||||||
'ACCOUNT_DISABLED' => 'Account is disabled and cannot log in',
|
'ACCOUNT_DISABLED' => 'Account is disabled and cannot log in',
|
||||||
'API_AUTH_TOKEN_SECRET is not configured' => 'API_AUTH_TOKEN_SECRET is not configured',
|
'API_AUTH_TOKEN_SECRET is not configured' => 'API_AUTH_TOKEN_SECRET is not configured',
|
||||||
|
'API_KEY is not configured' => 'API_KEY is not configured',
|
||||||
|
'Please provide api-key' => 'Please provide api-key',
|
||||||
|
'Invalid api-key' => 'Invalid api-key',
|
||||||
'AUTH_TOKEN_EXPIRED' => 'auth-token expired',
|
'AUTH_TOKEN_EXPIRED' => 'auth-token expired',
|
||||||
'AUTH_TOKEN_FORMAT_INVALID' => 'auth-token format invalid',
|
'AUTH_TOKEN_FORMAT_INVALID' => 'auth-token format invalid',
|
||||||
'AUTH_TOKEN_INVALID' => 'auth-token invalid',
|
'AUTH_TOKEN_INVALID' => 'auth-token invalid',
|
||||||
|
|||||||
@@ -4,6 +4,9 @@ declare(strict_types=1);
|
|||||||
return [
|
return [
|
||||||
'ACCOUNT_DISABLED' => '账号已被禁用,无法登录',
|
'ACCOUNT_DISABLED' => '账号已被禁用,无法登录',
|
||||||
'API_AUTH_TOKEN_SECRET is not configured' => '服务端未配置 API_AUTH_TOKEN_SECRET',
|
'API_AUTH_TOKEN_SECRET is not configured' => '服务端未配置 API_AUTH_TOKEN_SECRET',
|
||||||
|
'API_KEY is not configured' => '服务端未配置 API_KEY',
|
||||||
|
'Please provide api-key' => '请携带 api-key',
|
||||||
|
'Invalid api-key' => 'api-key 无效',
|
||||||
'AUTH_TOKEN_EXPIRED' => 'auth-token 已过期',
|
'AUTH_TOKEN_EXPIRED' => 'auth-token 已过期',
|
||||||
'AUTH_TOKEN_FORMAT_INVALID' => 'auth-token 格式无效',
|
'AUTH_TOKEN_FORMAT_INVALID' => 'auth-token 格式无效',
|
||||||
'AUTH_TOKEN_INVALID' => 'auth-token 无效',
|
'AUTH_TOKEN_INVALID' => 'auth-token 无效',
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
$options = getopt('', ['agent_id:', 'secret:', 'time::']);
|
$options = getopt('', ['agent_id:', 'secret:', 'time::']);
|
||||||
|
|
||||||
$agentId = $options['agent_id'] ?? '5ef059938ba799aaa845e1c2e8a762bd';
|
$agentId = $options['agent_id'] ?? '76dc611d6ebaafc66cc0879c71b5db5c';
|
||||||
$secret = $options['secret'] ?? 'xF75oK91TQj13s0UmNIr1NBWMWGfflNO';
|
$secret = $options['secret'] ?? 'xF75oK91TQj13s0UmNIr1NBWMWGfflNO';
|
||||||
$time = $options['time'] ?? (string) time();
|
$time = $options['time'] ?? (string) time();
|
||||||
|
|
||||||
|
|||||||