Compare commits
46 Commits
52b5ccb8e4
...
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 | |||
| dd264b1e97 | |||
| 085454fb78 |
1
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{}
|
||||
41
API对接文档.md
@@ -19,6 +19,7 @@
|
||||
- **请求方法**:项目路由多数使用 `Route::any`,对接建议统一使用 **POST**(便于 body 传参);个别接口文档中标注了 GET 参数。
|
||||
- **编码**:`UTF-8`
|
||||
- **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 统一返回结构
|
||||
|
||||
@@ -49,10 +50,10 @@
|
||||
|
||||
## 2. 鉴权与对接流程(平台侧 /api/v1)
|
||||
|
||||
平台侧接口分两步:
|
||||
平台侧接口需统一携带请求头 **`api-key`**(与服务端 `.env` 中 `API_KEY` 一致),业务接口另需 **`auth-token`**。
|
||||
|
||||
1. **获取 `auth-token`**
|
||||
2. **携带 `auth-token` 调用 `/api/v1/*` 业务接口**
|
||||
1. **获取 `auth-token`**(同时携带 `api-key`)
|
||||
2. **携带 `api-key` + `auth-token` 调用 `/api/v1/*` 业务接口**
|
||||
|
||||
### 2.1 获取 auth-token
|
||||
|
||||
@@ -100,11 +101,22 @@ signature = md5(agent_id + secret + time)
|
||||
- 密钥错误/签名错误/时间戳无效:`code=403`
|
||||
- 服务端未配置密钥或生成失败:`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。
|
||||
|
||||
@@ -116,7 +128,7 @@ signature = md5(agent_id + secret + time)
|
||||
|
||||
- **路径**:`/api/v1/getGameUrl`
|
||||
- **方法**:POST
|
||||
- **请求头**:`auth-token`
|
||||
- **请求头**:`api-key`、`auth-token`
|
||||
- **说明**:根据平台用户名创建/登录玩家并生成登录 JWT,返回可直接打开的游戏地址。
|
||||
|
||||
#### 请求参数(body)
|
||||
@@ -124,7 +136,6 @@ signature = md5(agent_id + secret + time)
|
||||
| 参数名 | 必填 | 类型 | 说明 |
|
||||
| --- | --- | --- | --- |
|
||||
| username | 是 | string | 玩家唯一账号(平台侧用户名) |
|
||||
| password | 否 | string | 默认 `123456` |
|
||||
| time | 否 | int/string | 默认当前时间戳 |
|
||||
| lang | 否 | string | `zh` / `en`,默认 `zh` |
|
||||
|
||||
@@ -144,7 +155,7 @@ signature = md5(agent_id + secret + time)
|
||||
|
||||
- **路径**:`/api/v1/getPlayerInfo`
|
||||
- **方法**:POST
|
||||
- **请求头**:`auth-token`
|
||||
- **请求头**:`api-key`、`auth-token`
|
||||
|
||||
#### 请求参数
|
||||
|
||||
@@ -160,7 +171,7 @@ signature = md5(agent_id + secret + time)
|
||||
|
||||
- **路径**:`/api/v1/getPlayerGameRecord`
|
||||
- **方法**:POST
|
||||
- **请求头**:`auth-token`
|
||||
- **请求头**:`api-key`、`auth-token`
|
||||
|
||||
#### 请求参数
|
||||
|
||||
@@ -181,7 +192,7 @@ signature = md5(agent_id + secret + time)
|
||||
|
||||
- **路径**:`/api/v1/getPlayerWalletRecord`
|
||||
- **方法**:POST
|
||||
- **请求头**:`auth-token`
|
||||
- **请求头**:`api-key`、`auth-token`
|
||||
|
||||
参数与时间规则同 3.3(无 `page`,仅 `limit` 限制条数),返回钱包流水列表(附带 `dice_player`)。
|
||||
|
||||
@@ -189,7 +200,7 @@ signature = md5(agent_id + secret + time)
|
||||
|
||||
- **路径**:`/api/v1/getPlayerTicketRecord`
|
||||
- **方法**:POST
|
||||
- **请求头**:`auth-token`
|
||||
- **请求头**:`api-key`、`auth-token`
|
||||
|
||||
参数与时间规则同 3.3,返回中奖券记录列表(附带 `dice_player`)。
|
||||
|
||||
@@ -197,7 +208,7 @@ signature = md5(agent_id + secret + time)
|
||||
|
||||
- **路径**:`/api/v1/setPlayerWallet`
|
||||
- **方法**:POST
|
||||
- **请求头**:`auth-token`
|
||||
- **请求头**:`api-key`、`auth-token`
|
||||
- **说明**:平台为玩家加币/扣币,生成钱包流水。
|
||||
|
||||
#### 请求参数
|
||||
@@ -256,9 +267,9 @@ signature = md5(agent_id + secret + time)
|
||||
| --- | --- | --- |
|
||||
| 200 | 成功 | 请求成功 |
|
||||
| 400 | 请求参数错误 | 缺参、参数格式不合法、范围错误 |
|
||||
| 401 | 未授权 | 未携带 `auth-token` 或 `token` |
|
||||
| 401 | 未授权 | 未携带 `api-key`、`auth-token` 或 `token` |
|
||||
| 402 | token 无效或已过期 | `auth-token/token` 过期、签名错误、被挤下线等 |
|
||||
| 403 | 鉴权失败 | `secret` 错误、签名验证失败、时间戳无效等 |
|
||||
| 403 | 鉴权失败 | `api-key` 无效、`secret` 错误、签名验证失败、时间戳无效等 |
|
||||
| 404 | 资源不存在 | 用户不存在等 |
|
||||
| 422 | 业务逻辑错误 | 余额不足、业务校验失败等 |
|
||||
| 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/` | 性能、权重测试、出点分析等专项说明(按需阅读)。 |
|
||||
|
||||
**与玩法直接相关的玩家接口示例**:
|
||||
|
||||
@@ -8,7 +8,8 @@
|
||||
},
|
||||
"scripts": {
|
||||
"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",
|
||||
"lint": "eslint",
|
||||
"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)')
|
||||
@@ -1,32 +1,38 @@
|
||||
import request from '@/utils/http'
|
||||
|
||||
export type DashboardQueryParams = {
|
||||
dept_id?: number
|
||||
/** 统计日期,格式 YYYY-MM-DD,默认当日 */
|
||||
date?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 大富翁工作台卡片统计(玩家注册、充值、提现、游玩次数,含较上周对比)
|
||||
* @returns 响应
|
||||
*/
|
||||
export function fetchStatistics() {
|
||||
export function fetchStatistics(params?: DashboardQueryParams) {
|
||||
return request.get<any>({
|
||||
url: '/core/dice/dashboard/statistics'
|
||||
url: '/core/dice/dashboard/statistics',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 近期玩家充值统计(近10天每日充值金额)
|
||||
* @returns 响应
|
||||
*/
|
||||
export function fetchRechargeChart() {
|
||||
export function fetchRechargeChart(params?: DashboardQueryParams) {
|
||||
return request.get<any>({
|
||||
url: '/core/dice/dashboard/rechargeChart'
|
||||
url: '/core/dice/dashboard/rechargeChart',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 月度玩家充值汇总(当年1-12月每月充值金额)
|
||||
* @returns 响应
|
||||
*/
|
||||
export function fetchRechargeBarChart() {
|
||||
export function fetchRechargeBarChart(params?: DashboardQueryParams) {
|
||||
return request.get<any>({
|
||||
url: '/core/dice/dashboard/rechargeBarChart'
|
||||
url: '/core/dice/dashboard/rechargeBarChart',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
@@ -39,11 +45,11 @@ export interface WalletRecordItem {
|
||||
|
||||
/**
|
||||
* 工作台-玩家充值记录(最新50条)
|
||||
* @returns 列表
|
||||
*/
|
||||
export function fetchWalletRecordList() {
|
||||
export function fetchWalletRecordList(params?: DashboardQueryParams) {
|
||||
return request.get<WalletRecordItem[]>({
|
||||
url: '/core/dice/dashboard/walletRecordList'
|
||||
url: '/core/dice/dashboard/walletRecordList',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
@@ -52,15 +58,34 @@ export interface NewPlayerItem {
|
||||
name: string
|
||||
coin: number
|
||||
total_ticket_count: number
|
||||
create_time: string
|
||||
}
|
||||
|
||||
/** 玩家游玩记录项 */
|
||||
export interface PlayRecordItem {
|
||||
player_name: string
|
||||
reward_tier: string
|
||||
reward_tier_label: string
|
||||
win_coin: number
|
||||
create_time: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 工作台-新增玩家记录(最新50条)
|
||||
* @returns 列表
|
||||
*/
|
||||
export function fetchNewPlayerList() {
|
||||
export function fetchNewPlayerList(params?: DashboardQueryParams) {
|
||||
return request.get<NewPlayerItem[]>({
|
||||
url: '/core/dice/dashboard/newPlayerList'
|
||||
url: '/core/dice/dashboard/newPlayerList',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 工作台-玩家游玩记录(最新50条)
|
||||
*/
|
||||
export function fetchPlayRecordList(params?: DashboardQueryParams) {
|
||||
return request.get<PlayRecordItem[]>({
|
||||
url: '/core/dice/dashboard/playRecordList',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
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>) {
|
||||
return request.del<any>({
|
||||
url: '/core/dept/destroy',
|
||||
data: params
|
||||
data: params,
|
||||
showErrorMessage: false
|
||||
})
|
||||
},
|
||||
|
||||
@@ -71,5 +72,19 @@ export default {
|
||||
return request.get<Api.Common.ApiData[]>({
|
||||
url: '/core/dept/accessDept'
|
||||
})
|
||||
},
|
||||
|
||||
destroyPreview(ids: string | number | Array<string | number>) {
|
||||
const idStr = Array.isArray(ids) ? ids.join(',') : String(ids)
|
||||
return request.get<Api.Common.ApiData>({
|
||||
url: '/core/dept/destroyPreview',
|
||||
params: { ids: idStr }
|
||||
})
|
||||
},
|
||||
|
||||
syncChannelConfigs() {
|
||||
return request.post<any>({
|
||||
url: '/core/dept/syncChannelConfigs'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,75 +0,0 @@
|
||||
import request from '@/utils/http'
|
||||
|
||||
/**
|
||||
* 岗位API
|
||||
*/
|
||||
export default {
|
||||
/**
|
||||
* 获取数据列表
|
||||
* @param params 搜索参数
|
||||
* @returns 数据列表
|
||||
*/
|
||||
list(params: Record<string, any>) {
|
||||
return request.get<Api.Common.ApiPage>({
|
||||
url: '/core/post/index',
|
||||
params
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 读取数据
|
||||
* @param id 数据ID
|
||||
* @returns 数据详情
|
||||
*/
|
||||
read(id: number | string) {
|
||||
return request.get<Api.Common.ApiData>({
|
||||
url: '/core/post/read?id=' + id
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 创建数据
|
||||
* @param params 数据参数
|
||||
* @returns 执行结果
|
||||
*/
|
||||
save(params: Record<string, any>) {
|
||||
return request.post<any>({
|
||||
url: '/core/post/save',
|
||||
data: params
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 更新数据
|
||||
* @param params 数据参数
|
||||
* @returns 执行结果
|
||||
*/
|
||||
update(params: Record<string, any>) {
|
||||
return request.put<any>({
|
||||
url: '/core/post/update',
|
||||
data: params
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 删除数据
|
||||
* @param id 数据ID
|
||||
* @returns 执行结果
|
||||
*/
|
||||
delete(params: Record<string, any>) {
|
||||
return request.del<any>({
|
||||
url: '/core/post/destroy',
|
||||
data: params
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 可操作岗位
|
||||
* @returns 数据列表
|
||||
*/
|
||||
accessPost() {
|
||||
return request.get<Api.Common.ApiData[]>({
|
||||
url: '/core/post/accessPost'
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -79,9 +79,10 @@ export default {
|
||||
* 可操作角色
|
||||
* @returns 数据列表
|
||||
*/
|
||||
accessRole() {
|
||||
accessRole(params?: Record<string, unknown>) {
|
||||
return request.get<Api.Common.ApiData[]>({
|
||||
url: '/core/role/accessRole'
|
||||
url: '/core/role/accessRole',
|
||||
params
|
||||
})
|
||||
},
|
||||
|
||||
|
||||
144
saiadmin-artd/src/components/channel/SuperAdminChannelShell.vue
Normal file
@@ -0,0 +1,144 @@
|
||||
<template>
|
||||
<div v-if="props.enabled" class="art-full-height super-admin-channel-shell">
|
||||
<div class="box-border flex gap-3 h-full max-md:block max-md:gap-0 max-md:h-auto">
|
||||
<div class="channel-list-panel flex-shrink-0 h-full max-md:w-full max-md:h-auto max-md:mb-5">
|
||||
<ElCard
|
||||
class="channel-tree-card tree-card art-card-xs flex flex-col h-full mt-0"
|
||||
shadow="never"
|
||||
v-loading="loadingChannels"
|
||||
>
|
||||
<template #header>
|
||||
<b class="channel-list-title">{{ $t('common.channelScope.listTitle') }}</b>
|
||||
</template>
|
||||
<ElScrollbar>
|
||||
<ElTree
|
||||
:data="displayTreeData"
|
||||
:props="{ children: 'children', label: 'label' }"
|
||||
node-key="id"
|
||||
:current-node-key="selectedDeptId"
|
||||
default-expand-all
|
||||
highlight-current
|
||||
@node-click="onNodeClick"
|
||||
/>
|
||||
</ElScrollbar>
|
||||
</ElCard>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col flex-grow min-w-0 min-h-0">
|
||||
<div v-if="selectedDisplayLabel" class="channel-banner mb-3 text-sm text-g-500">
|
||||
{{ bannerLabel }}:<b>{{ selectedDisplayLabel }}</b>
|
||||
</div>
|
||||
<div class="flex flex-col flex-1 min-h-0 min-w-0">
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<slot v-else />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { isRoleChannelRoute } from '@/utils/channelLayout'
|
||||
import {
|
||||
DEFAULT_CHANNEL_ID,
|
||||
useChannelDeptScope,
|
||||
type ChannelTreeNode
|
||||
} from '@/composables/useChannelDeptScope'
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
defineOptions({ name: 'SuperAdminChannelShell' })
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
enabled?: boolean
|
||||
}>(),
|
||||
{
|
||||
enabled: true
|
||||
}
|
||||
)
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const {
|
||||
treeData,
|
||||
selectedDeptId,
|
||||
loadingChannels,
|
||||
handleChannelClick,
|
||||
provideScope,
|
||||
isConfigScope,
|
||||
isAllChannelScope,
|
||||
showDefaultTemplate
|
||||
} = useChannelDeptScope()
|
||||
|
||||
provideScope()
|
||||
|
||||
const displayTreeData = computed(() => {
|
||||
const nodes = showDefaultTemplate.value
|
||||
? treeData.value
|
||||
: treeData.value.filter((n) => n.id !== DEFAULT_CHANNEL_ID)
|
||||
const defaultLabel = isRoleChannelRoute(route)
|
||||
? t('common.channelScope.defaultRoleTemplate')
|
||||
: t('common.channelScope.defaultTemplate')
|
||||
const emptyNodeLabel = isAllChannelScope.value ? t('common.channelScope.allChannels') : defaultLabel
|
||||
return nodes.map((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(() => {
|
||||
if (isConfigScope.value) {
|
||||
return t('common.channelScope.currentConfig')
|
||||
}
|
||||
if (isRoleChannelRoute(route)) {
|
||||
return t('common.channelScope.currentRole')
|
||||
}
|
||||
return t('common.channelScope.currentChannel')
|
||||
})
|
||||
|
||||
const onNodeClick = (data: ChannelTreeNode) => {
|
||||
handleChannelClick(data)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.super-admin-channel-shell {
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
/* 原 w-64(16rem) 的 0.6 倍 */
|
||||
.channel-list-panel {
|
||||
width: 9.6rem;
|
||||
}
|
||||
|
||||
.channel-tree-card :deep(.el-card__header) {
|
||||
padding: 10px 12px;
|
||||
}
|
||||
|
||||
.channel-list-title {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.channel-tree-card :deep(.el-tree-node__content) {
|
||||
height: 30px;
|
||||
padding-right: 4px;
|
||||
}
|
||||
|
||||
.channel-tree-card :deep(.el-tree-node__label) {
|
||||
font-size: 13px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.channel-banner b {
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
</style>
|
||||
@@ -18,22 +18,34 @@
|
||||
<!-- 缓存路由动画 -->
|
||||
<Transition :name="showTransitionMask ? '' : actualTransition" mode="out-in" appear>
|
||||
<KeepAlive :max="10" :exclude="keepAliveExclude">
|
||||
<SuperAdminChannelShell
|
||||
v-if="route.meta.keepAlive && shouldWrapChannelLayout(route)"
|
||||
:key="'ch-' + route.path"
|
||||
>
|
||||
<component class="art-page-view" :is="Component" :key="route.path" />
|
||||
</SuperAdminChannelShell>
|
||||
<component
|
||||
v-else-if="route.meta.keepAlive"
|
||||
class="art-page-view"
|
||||
:is="Component"
|
||||
:key="route.path"
|
||||
v-if="route.meta.keepAlive"
|
||||
/>
|
||||
</KeepAlive>
|
||||
</Transition>
|
||||
|
||||
<!-- 非缓存路由动画 -->
|
||||
<Transition :name="showTransitionMask ? '' : actualTransition" mode="out-in" appear>
|
||||
<SuperAdminChannelShell
|
||||
v-if="!route.meta.keepAlive && shouldWrapChannelLayout(route)"
|
||||
:key="'ch-' + route.path"
|
||||
>
|
||||
<component class="art-page-view" :is="Component" :key="route.path" />
|
||||
</SuperAdminChannelShell>
|
||||
<component
|
||||
v-else-if="!route.meta.keepAlive"
|
||||
class="art-page-view"
|
||||
:is="Component"
|
||||
:key="route.path"
|
||||
v-if="!route.meta.keepAlive"
|
||||
/>
|
||||
</Transition>
|
||||
</RouterView>
|
||||
@@ -53,6 +65,8 @@
|
||||
import { useAutoLayoutHeight } from '@/hooks/core/useLayoutHeight'
|
||||
import { useSettingStore } from '@/store/modules/setting'
|
||||
import { useWorktabStore } from '@/store/modules/worktab'
|
||||
import SuperAdminChannelShell from '@/components/channel/SuperAdminChannelShell.vue'
|
||||
import { shouldWrapSuperAdminChannelLayout as shouldWrapChannelLayout } from '@/utils/channelLayout'
|
||||
|
||||
defineOptions({ name: 'ArtPageContent' })
|
||||
|
||||
|
||||
12
saiadmin-artd/src/components/dice/ChannelConfigLayout.vue
Normal file
@@ -0,0 +1,12 @@
|
||||
<!-- 兼容旧引用:超管渠道栏已提升至全局 SuperAdminChannelShell -->
|
||||
<template>
|
||||
<slot :dept-id="deptId" :dept-params="deptParams" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useInjectedChannelDept, DEFAULT_CHANNEL_ID } from '@/composables/useChannelDeptScope'
|
||||
|
||||
const channel = useInjectedChannelDept()
|
||||
const deptId = computed(() => channel?.selectedDeptId.value ?? DEFAULT_CHANNEL_ID)
|
||||
const deptParams = computed(() => ({ dept_id: deptId.value }))
|
||||
</script>
|
||||
6
saiadmin-artd/src/composables/useChannelConfigScope.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
/** @deprecated 请使用 useChannelDeptScope */
|
||||
export {
|
||||
DEFAULT_CHANNEL_ID,
|
||||
useChannelDeptScope as useChannelConfigScope,
|
||||
type ChannelTreeNode
|
||||
} from './useChannelDeptScope'
|
||||
289
saiadmin-artd/src/composables/useChannelDeptScope.ts
Normal file
@@ -0,0 +1,289 @@
|
||||
import type { InjectionKey, Ref, ComputedRef } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import deptApi from '@/api/system/dept'
|
||||
import { router } from '@/router'
|
||||
import { useUserStore } from '@/store/modules/user'
|
||||
import {
|
||||
isAllChannelScopeRoute,
|
||||
isConfigChannelRoute,
|
||||
isNoChannelLayoutRoute,
|
||||
isRoleChannelRoute,
|
||||
isSuperAdminUser
|
||||
} from '@/utils/channelLayout'
|
||||
|
||||
export interface ChannelTreeNode {
|
||||
id: number
|
||||
label: string
|
||||
children?: ChannelTreeNode[]
|
||||
}
|
||||
|
||||
/** 默认配置模板(dept_id = 0) */
|
||||
export const DEFAULT_CHANNEL_ID = 0
|
||||
|
||||
export interface ChannelDeptScopeContext {
|
||||
treeData: Ref<ChannelTreeNode[]>
|
||||
selectedDeptId: Ref<number>
|
||||
loadingChannels: Ref<boolean>
|
||||
isSuperAdmin: Ref<boolean>
|
||||
selectedDeptLabel: Ref<string>
|
||||
deptQueryParams: ComputedRef<{ dept_id: number }>
|
||||
isConfigScope: ComputedRef<boolean>
|
||||
isAllChannelScope: ComputedRef<boolean>
|
||||
showDefaultTemplate: ComputedRef<boolean>
|
||||
}
|
||||
|
||||
export const CHANNEL_DEPT_SCOPE_KEY: InjectionKey<ChannelDeptScopeContext> =
|
||||
Symbol('channelDeptScope')
|
||||
|
||||
/**
|
||||
* 当前应用中处于激活态的超管渠道上下文。
|
||||
* 仅有一个 SuperAdminChannelShell 实例,所以可以在模块作用域内缓存其 ctx,
|
||||
* 供 setInterval / 异步回调 / 事件处理器等"setup 外"路径取用——
|
||||
* 这些路径上 Vue 的 inject() 会失效(getCurrentInstance() 为 null)。
|
||||
*/
|
||||
let activeChannelCtx: ChannelDeptScopeContext | null = null
|
||||
|
||||
export function provideChannelDeptScope(ctx: ChannelDeptScopeContext) {
|
||||
provide(CHANNEL_DEPT_SCOPE_KEY, ctx)
|
||||
activeChannelCtx = ctx
|
||||
onScopeDispose(() => {
|
||||
if (activeChannelCtx === ctx) {
|
||||
activeChannelCtx = null
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export function useInjectedChannelDept(): ChannelDeptScopeContext | null {
|
||||
// 仅在组件 setup 同步路径下调用 inject() 才可靠;
|
||||
// 否则(异步回调、setInterval、事件处理器等)退化为读取模块级激活上下文,
|
||||
// 避免渠道切换后 getCurrentPool/withChannelDeptParams 等仍按超管自身部门发请求。
|
||||
if (getCurrentInstance()) {
|
||||
const ctx = inject(CHANNEL_DEPT_SCOPE_KEY, null)
|
||||
if (ctx) {
|
||||
return ctx
|
||||
}
|
||||
}
|
||||
return activeChannelCtx
|
||||
}
|
||||
|
||||
/** 超管全局渠道栏:创建并 provide 渠道上下文 */
|
||||
export function useChannelDeptScope() {
|
||||
const route = useRoute()
|
||||
const userStore = useUserStore()
|
||||
const treeData = ref<ChannelTreeNode[]>([])
|
||||
const selectedDeptId = ref<number>(DEFAULT_CHANNEL_ID)
|
||||
const loadingChannels = ref(false)
|
||||
|
||||
const isConfigScope = computed(() => isConfigChannelRoute(route))
|
||||
const isRoleScope = computed(() => isRoleChannelRoute(route))
|
||||
const isAllChannelScope = computed(() => isAllChannelScopeRoute(route))
|
||||
const showDefaultTemplate = computed(() => isConfigScope.value || isRoleScope.value)
|
||||
|
||||
const isSuperAdmin = computed(() => isSuperAdminUser())
|
||||
|
||||
const selectedDeptLabel = computed(() => {
|
||||
const find = (nodes: ChannelTreeNode[]): string => {
|
||||
for (const n of nodes) {
|
||||
if (n.id === selectedDeptId.value) {
|
||||
return n.label
|
||||
}
|
||||
if (n.children?.length) {
|
||||
const sub = find(n.children)
|
||||
if (sub) return sub
|
||||
}
|
||||
}
|
||||
return ''
|
||||
}
|
||||
return find(treeData.value)
|
||||
})
|
||||
|
||||
const deptQueryParams = computed(() => {
|
||||
const id = selectedDeptId.value
|
||||
if (isAllChannelScope.value && id <= 0) {
|
||||
return { dept_id: 0 }
|
||||
}
|
||||
if (!showDefaultTemplate.value && id <= 0) {
|
||||
return { dept_id: 0 }
|
||||
}
|
||||
return { dept_id: id }
|
||||
})
|
||||
|
||||
const loadChannels = async () => {
|
||||
loadingChannels.value = true
|
||||
try {
|
||||
const list = await deptApi.accessDept()
|
||||
const channels = Array.isArray(list) ? list : []
|
||||
const nodes: ChannelTreeNode[] = channels.map((item: Record<string, unknown>) => ({
|
||||
id: Number(item.id ?? item.value),
|
||||
label: String(item.label ?? item.name ?? item.id)
|
||||
}))
|
||||
if (isSuperAdmin.value) {
|
||||
if (showDefaultTemplate.value || isAllChannelScope.value) {
|
||||
treeData.value = [{ id: DEFAULT_CHANNEL_ID, label: '' }, ...nodes]
|
||||
if (!treeData.value.some((n) => n.id === selectedDeptId.value)) {
|
||||
selectedDeptId.value = DEFAULT_CHANNEL_ID
|
||||
}
|
||||
} else {
|
||||
treeData.value = nodes
|
||||
if (nodes.length > 0) {
|
||||
const valid = nodes.some((n) => n.id === selectedDeptId.value)
|
||||
if (!valid || selectedDeptId.value <= 0) {
|
||||
selectedDeptId.value = nodes[0].id
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
treeData.value = nodes
|
||||
if (nodes.length > 0) {
|
||||
selectedDeptId.value = nodes[0].id
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
loadingChannels.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleChannelClick = (data: ChannelTreeNode) => {
|
||||
selectedDeptId.value = Number(data.id)
|
||||
}
|
||||
|
||||
const ctx: ChannelDeptScopeContext = {
|
||||
treeData,
|
||||
selectedDeptId,
|
||||
loadingChannels,
|
||||
isSuperAdmin,
|
||||
selectedDeptLabel,
|
||||
deptQueryParams,
|
||||
isConfigScope,
|
||||
isAllChannelScope,
|
||||
showDefaultTemplate
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadChannels()
|
||||
})
|
||||
|
||||
return {
|
||||
...ctx,
|
||||
loadChannels,
|
||||
handleChannelClick,
|
||||
provideScope: () => provideChannelDeptScope(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
/** 将当前选中渠道写入列表查询参数并刷新 */
|
||||
export function bindChannelDeptToSearchParams(
|
||||
searchParams: Record<string, unknown>,
|
||||
refresh: () => void,
|
||||
options?: { immediate?: boolean; enabled?: boolean }
|
||||
) {
|
||||
const channel = useInjectedChannelDept()
|
||||
if (!channel || options?.enabled === false) {
|
||||
return
|
||||
}
|
||||
|
||||
const apply = (deptId: number) => {
|
||||
if (channel.isAllChannelScope.value && deptId <= 0) {
|
||||
delete searchParams.dept_id
|
||||
refresh()
|
||||
return
|
||||
}
|
||||
if (!channel.showDefaultTemplate.value && deptId <= 0) {
|
||||
return
|
||||
}
|
||||
searchParams.dept_id = deptId
|
||||
refresh()
|
||||
}
|
||||
|
||||
watch(
|
||||
() => channel.selectedDeptId.value,
|
||||
(deptId) => apply(deptId),
|
||||
{ immediate: options?.immediate ?? true }
|
||||
)
|
||||
}
|
||||
|
||||
/** 工作台等非 useTable 页面:渠道切换时重新拉数 */
|
||||
export function useChannelDeptReload(loadFn: () => void | Promise<void>) {
|
||||
const channel = useInjectedChannelDept()
|
||||
if (!channel) {
|
||||
onMounted(() => {
|
||||
void loadFn()
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
watch(
|
||||
() => channel.selectedDeptId.value,
|
||||
(deptId) => {
|
||||
if (channel.isAllChannelScope.value && deptId <= 0) {
|
||||
void loadFn()
|
||||
return
|
||||
}
|
||||
if (!channel.showDefaultTemplate.value && deptId <= 0) {
|
||||
return
|
||||
}
|
||||
void loadFn()
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
}
|
||||
|
||||
/** 请求参数:业务页附带 dept_id;渠道管理员固定本渠道 */
|
||||
export function getChannelDeptRequestParams(): { dept_id?: number } {
|
||||
const channel = useInjectedChannelDept()
|
||||
const route = getCurrentInstance() ? useRoute() : router.currentRoute.value
|
||||
if (isNoChannelLayoutRoute(route)) {
|
||||
return {}
|
||||
}
|
||||
if (channel?.isSuperAdmin.value) {
|
||||
const deptId = channel.selectedDeptId.value
|
||||
if (channel.isAllChannelScope.value && deptId <= 0) {
|
||||
return {}
|
||||
}
|
||||
if (!channel.showDefaultTemplate.value && deptId <= 0) {
|
||||
return {}
|
||||
}
|
||||
if (deptId > 0) {
|
||||
return { dept_id: deptId }
|
||||
}
|
||||
if (channel.showDefaultTemplate.value) {
|
||||
return { dept_id: deptId }
|
||||
}
|
||||
return {}
|
||||
}
|
||||
|
||||
const userStore = useUserStore()
|
||||
const dept = userStore.info?.department
|
||||
if (dept && Number(dept.id) > 0) {
|
||||
return { dept_id: Number(dept.id) }
|
||||
}
|
||||
|
||||
return {}
|
||||
}
|
||||
|
||||
/** 保存/更新时附带 dept_id(新增优先渠道栏;更新优先行内 dept_id,避免默认模板 0 覆盖真实渠道) */
|
||||
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()
|
||||
if ('dept_id' in extra) {
|
||||
return { ...payload, ...extra }
|
||||
}
|
||||
if (hasRowDept && Number.isFinite(rowDeptNum) && rowDeptNum > 0) {
|
||||
return { ...payload, dept_id: rowDeptNum }
|
||||
}
|
||||
const channel = useInjectedChannelDept()
|
||||
if (channel && channel.selectedDeptId.value > 0) {
|
||||
return { ...payload, dept_id: channel.selectedDeptId.value }
|
||||
}
|
||||
return payload
|
||||
}
|
||||
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 }
|
||||
)
|
||||
}
|
||||
@@ -35,11 +35,16 @@ import {
|
||||
createErrorHandler
|
||||
} from '../../utils/table/tableUtils'
|
||||
import { tableConfig } from '../../utils/table/tableConfig'
|
||||
import { bindChannelDeptToSearchParams, useInjectedChannelDept, getChannelDeptRequestParams } from '@/composables/useChannelDeptScope'
|
||||
|
||||
// 类型推导工具类型
|
||||
type InferApiParams<T> = T extends (params: infer P) => any ? P : 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<
|
||||
@@ -441,6 +446,23 @@ function useTableImpl<TApiFn extends (params: any) => Promise<any>>(
|
||||
// 智能防抖搜索函数
|
||||
const debouncedGetDataByPage = createSmartDebounce(getDataByPage, debounceTime)
|
||||
|
||||
const channelScope = useInjectedChannelDept()
|
||||
const hasChannelScope = !!channelScope
|
||||
bindChannelDeptToSearchParams(
|
||||
searchParams as Record<string, unknown>,
|
||||
() => {
|
||||
void getDataByPage()
|
||||
},
|
||||
{ immediate: hasChannelScope }
|
||||
)
|
||||
|
||||
if (!hasChannelScope) {
|
||||
const channelDeptParams = getChannelDeptRequestParams()
|
||||
if (channelDeptParams.dept_id !== undefined) {
|
||||
Object.assign(searchParams as Record<string, unknown>, channelDeptParams)
|
||||
}
|
||||
}
|
||||
|
||||
// 重置搜索参数
|
||||
const resetSearchParams = async (): Promise<void> => {
|
||||
// 取消防抖的搜索
|
||||
@@ -645,7 +667,7 @@ function useTableImpl<TApiFn extends (params: any) => Promise<any>>(
|
||||
}
|
||||
|
||||
// 挂载时自动加载数据
|
||||
if (immediate) {
|
||||
if (immediate && !hasChannelScope) {
|
||||
onMounted(async () => {
|
||||
await getData()
|
||||
})
|
||||
|
||||
@@ -37,7 +37,16 @@
|
||||
"tips": "Prompt",
|
||||
"cancel": "Cancel",
|
||||
"confirm": "Confirm",
|
||||
"logOutTips": "Do you want to log out?"
|
||||
"logOutTips": "Do you want to log out?",
|
||||
"channelScope": {
|
||||
"listTitle": "Channels",
|
||||
"defaultTemplate": "Default template",
|
||||
"defaultRoleTemplate": "Default role template",
|
||||
"allChannels": "All",
|
||||
"currentConfig": "Current config",
|
||||
"currentChannel": "Current channel",
|
||||
"currentRole": "Current roles"
|
||||
}
|
||||
},
|
||||
"uiMsg": {
|
||||
"titlePrompt": "Prompt",
|
||||
@@ -291,23 +300,51 @@
|
||||
"gohome": "Go Home"
|
||||
},
|
||||
"console": {
|
||||
"filter": {
|
||||
"date": "Statistics Date",
|
||||
"placeholder": "Select date",
|
||||
"today": "Today",
|
||||
"clear": "Clear"
|
||||
},
|
||||
"card": {
|
||||
"playerRegister": "Player Register",
|
||||
"playerCharge": "Player Charge",
|
||||
"playerWithdraw": "Player Withdraw",
|
||||
"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": {
|
||||
"title": "New Players",
|
||||
"subtitle": "Latest 50 new player records",
|
||||
"subtitleByDate": "New players on {date} (up to 50)",
|
||||
"player": "Player",
|
||||
"balance": "Balance",
|
||||
"ticket": "Tickets"
|
||||
"ticket": "Tickets",
|
||||
"registerTime": "Register Time"
|
||||
},
|
||||
"playRecord": {
|
||||
"title": "Player Play Records",
|
||||
"subtitle": "Latest 50 play records",
|
||||
"subtitleByDate": "Play records on {date} (up to 50)",
|
||||
"player": "Player",
|
||||
"reward": "Reward Tier",
|
||||
"winCoin": "Win Amount",
|
||||
"playTime": "Play Time"
|
||||
},
|
||||
"walletRecord": {
|
||||
"title": "Player Charge Records",
|
||||
"subtitle": "Latest 50 charge records",
|
||||
"subtitleByDate": "Charge records on {date} (up to 50)",
|
||||
"player": "Player",
|
||||
"chargeAmount": "Amount",
|
||||
"chargeTime": "Charge Time"
|
||||
@@ -378,8 +415,7 @@
|
||||
"role": "Role Management",
|
||||
"userCenter": "User Center",
|
||||
"menu": "Menu Management",
|
||||
"dept": "Department Management",
|
||||
"post": "Post Management",
|
||||
"dept": "Channel Management",
|
||||
"config": "System Config"
|
||||
},
|
||||
"safeguard": {
|
||||
@@ -411,6 +447,9 @@
|
||||
"rewardConfigRecord": "Dice Weight Test Records",
|
||||
"playRecordTest": "Draw Records (Test Weight)",
|
||||
"config": "Game Config"
|
||||
},
|
||||
"game": {
|
||||
"title": "Game Management"
|
||||
}
|
||||
},
|
||||
"table": {
|
||||
@@ -430,6 +469,10 @@
|
||||
"max": "Max",
|
||||
"startTime": "Start Time",
|
||||
"endTime": "End Time",
|
||||
"quickDate": "Quick date",
|
||||
"quickToday": "Today",
|
||||
"quickYesterday": "Yesterday",
|
||||
"quickLast7Days": "Last 7 days",
|
||||
"placeholderUsername": "Username",
|
||||
"placeholderNickname": "Nickname",
|
||||
"placeholderPhone": "Phone",
|
||||
@@ -445,8 +488,6 @@
|
||||
"placeholderDeptCode": "Dept Code",
|
||||
"placeholderRoleName": "Role Name",
|
||||
"placeholderRoleCode": "Role Code",
|
||||
"placeholderPostName": "Post Name",
|
||||
"placeholderPostCode": "Post Code",
|
||||
"placeholderMenuName": "Menu Name",
|
||||
"placeholderMenuRoute": "Menu Route",
|
||||
"placeholderOperator": "Operator",
|
||||
@@ -510,14 +551,12 @@
|
||||
"system": {
|
||||
"username": "Username",
|
||||
"phone": "Phone",
|
||||
"dept": "Department",
|
||||
"dept": "Channel",
|
||||
"dashboard": "Dashboard",
|
||||
"loginTime": "Last Login",
|
||||
"agentId": "Agent ID",
|
||||
"postName": "Post Name",
|
||||
"postCode": "Post Code",
|
||||
"deptName": "Dept Name",
|
||||
"deptCode": "Dept Code",
|
||||
"deptName": "Channel Name",
|
||||
"deptCode": "Channel Code",
|
||||
"leader": "Leader",
|
||||
"roleName": "Role Name",
|
||||
"roleCode": "Role Code",
|
||||
|
||||
@@ -27,6 +27,8 @@
|
||||
"labelIsDefault": "Default Ante",
|
||||
"placeholderName": "Please enter name",
|
||||
"placeholderTitle": "Please enter title",
|
||||
"placeholderNameAuto": "Auto from multiplier, e.g. x5",
|
||||
"placeholderTitleAuto": "Auto from multiplier, e.g. x5",
|
||||
"ruleNameRequired": "Please enter name",
|
||||
"ruleTitleRequired": "Please enter title",
|
||||
"ruleMultRequired": "Please enter ante multiplier",
|
||||
|
||||
68
saiadmin-artd/src/locales/langs/en/dice/game.json
Normal file
@@ -0,0 +1,68 @@
|
||||
{
|
||||
"form": {
|
||||
"dialogTitleAdd": "Add Game",
|
||||
"dialogTitleEdit": "Edit Game",
|
||||
"provider": "Provider",
|
||||
"placeholderProvider": "Enter provider name",
|
||||
"providerCode": "Provider Code",
|
||||
"placeholderProviderCode": "Enter provider code",
|
||||
"gameCode": "Game Code",
|
||||
"placeholderGameCode": "Enter game code",
|
||||
"gameKey": "Game Key",
|
||||
"placeholderGameKey": "Enter unique game key",
|
||||
"gameName": "Name (ZH)",
|
||||
"placeholderGameName": "Enter Chinese name",
|
||||
"gameNameEn": "Name (EN)",
|
||||
"placeholderGameNameEn": "Enter English name",
|
||||
"gameType": "Game Type",
|
||||
"placeholderGameType": "Enter game type",
|
||||
"sort": "Sort",
|
||||
"logo": "Logo URL",
|
||||
"tabPicker": "Pick Image",
|
||||
"tabUpload": "Upload Image",
|
||||
"gameUrl": "Game URL",
|
||||
"placeholderGameUrl": "Enter game URL",
|
||||
"hallUrl": "Hall URL",
|
||||
"placeholderHallUrl": "Enter hall URL",
|
||||
"status": "Status",
|
||||
"statusEnabled": "Enabled",
|
||||
"statusDisabled": "Disabled",
|
||||
"remark": "Remark",
|
||||
"placeholderRemark": "Enter remark",
|
||||
"addSuccess": "Added successfully",
|
||||
"editSuccess": "Updated successfully",
|
||||
"ruleProviderRequired": "Provider is required",
|
||||
"ruleProviderCodeRequired": "Provider code is required",
|
||||
"ruleGameCodeRequired": "Game code is required",
|
||||
"ruleGameKeyRequired": "Game key is required",
|
||||
"ruleGameNameRequired": "Chinese name is required",
|
||||
"ruleGameTypeRequired": "Game type is required"
|
||||
},
|
||||
"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",
|
||||
"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",
|
||||
"placeholderName": "Please enter name",
|
||||
"placeholderRemark": "Please enter remark",
|
||||
"placeholderPoolName": "Pool display name, e.g. Normal pool",
|
||||
"placeholderConfigNote": "Optional notes about this pool",
|
||||
"poolType": "Pool Type",
|
||||
"placeholderPoolType": "Please select pool type",
|
||||
"poolTypeNormal": "Normal",
|
||||
"poolTypeKill": "Kill",
|
||||
"poolTypeFree": "Free",
|
||||
"poolTypeKill": "Force score kill",
|
||||
"poolTypeT1": "T1 High",
|
||||
"safetyLine": "Safety Line",
|
||||
"t1Weight": "T1 Pool Weight (%)",
|
||||
@@ -21,19 +24,22 @@
|
||||
"currentPoolTitle": "Current Lottery Pool",
|
||||
"loading": "Loading...",
|
||||
"poolName": "Pool Name",
|
||||
"playerProfit": "Player Total Profit (profit_amount):",
|
||||
"poolProfitAmount": "Pool cumulative profit (profit_amount):",
|
||||
"realtime": "Live",
|
||||
"profitCalcHint": "Profit per round: paid = win_coin (incl. BIGWIN) - paid_amount (= ante×1); free = win_coin. Refreshes every 2s while open.",
|
||||
"tierRuleTitle": "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).",
|
||||
"killScoreWeights": "Kill weights",
|
||||
"killWeightNote": "(Kill weights from pool config type=1; edit in list.)",
|
||||
"btnResetProfit": "Reset Player Total Profit",
|
||||
"btnSaveSafetyLine": "Save Safety Line",
|
||||
"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": "Paid draw tier rule",
|
||||
"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",
|
||||
"killScoreWeights": "Kill weights (killScore)",
|
||||
"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 pool cumulative profit",
|
||||
"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",
|
||||
"msgGetPoolFailed": "Failed to get lottery pool",
|
||||
"msgSaveSuccess": "Save Success",
|
||||
"msgResetProfitSuccess": "Player total profit reset to 0",
|
||||
"msgResetProfitSuccess": "Pool cumulative profit reset to 0",
|
||||
"msgResetFailed": "Reset failed",
|
||||
"ruleNameRequired": "Name is required",
|
||||
"rulePoolTypeRequired": "Please select pool type",
|
||||
@@ -54,13 +60,18 @@
|
||||
"placeholderName": "Please enter name",
|
||||
"placeholderPoolType": "Please select pool type",
|
||||
"poolTypeNormal": "Normal",
|
||||
"poolTypeKill": "Force Kill",
|
||||
"poolTypeFree": "Free",
|
||||
"poolTypeKill": "Force Score Kill",
|
||||
"poolTypeT1": "T1 High Rate"
|
||||
},
|
||||
"table": {
|
||||
"name": "Name",
|
||||
"name": "Code",
|
||||
"poolName": "Pool Name",
|
||||
"configNote": "Remark",
|
||||
"poolType": "Pool Type",
|
||||
"safetyLine": "Safety Line",
|
||||
"safetyLineNotUsed": "Not used for kill",
|
||||
"safetyLineTip": "Normal (default) row only",
|
||||
"t1PoolWeight": "T1 Pool Weight",
|
||||
"t2PoolWeight": "T2 Pool Weight",
|
||||
"t3PoolWeight": "T3 Pool Weight",
|
||||
|
||||
@@ -34,7 +34,15 @@
|
||||
"placeholderRewardTier": "Select reward tier",
|
||||
"addSuccess": "Added successfully",
|
||||
"editSuccess": "Updated successfully",
|
||||
"validateFailed": "Validation failed, please check required fields and format"
|
||||
"validateFailed": "Validation failed, please check required fields and format",
|
||||
"rulePlayerRequired": "Please select player",
|
||||
"ruleLotteryConfigRequired": "Please select lottery pool config",
|
||||
"ruleLotteryTypeRequired": "Please select draw type",
|
||||
"ruleIsWinRequired": "Please select big win status",
|
||||
"ruleWinCoinRequired": "Win coin is required",
|
||||
"ruleRollArrayLength": "Roll array must have 5 numbers",
|
||||
"ruleRollArrayValues": "Enter 5 numbers, each between 1 and 6",
|
||||
"ruleRewardTierRequired": "Please select reward tier"
|
||||
},
|
||||
"toolbar": {
|
||||
"platformTotalProfit": "Platform Total Profit"
|
||||
@@ -49,6 +57,7 @@
|
||||
"rollNumber": "Roll Number",
|
||||
"rewardTier": "Reward Tier",
|
||||
"rewardConfig": "Reward Config",
|
||||
"createTime": "Created At",
|
||||
"usernameFuzzy": "Username (fuzzy)",
|
||||
"nameFuzzy": "Name (fuzzy)",
|
||||
"uiTextFuzzy": "UI Text (fuzzy)",
|
||||
@@ -76,6 +85,7 @@
|
||||
"rollArray": "Roll Array",
|
||||
"rollNumber": "Roll Number",
|
||||
"rewardTier": "Reward Tier",
|
||||
"remark": "Remark",
|
||||
"createTime": "Create Time",
|
||||
"updateTime": "Update Time"
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
"platformTotalProfit": "Platform Total Profit"
|
||||
},
|
||||
"search": {
|
||||
"lotteryPoolConfig": "Lottery Pool Config",
|
||||
"placeholderLotteryPool": "Select pool (search by name)",
|
||||
"rewardConfigRecordId": "Weight Test Record ID",
|
||||
"drawType": "Draw Type",
|
||||
"direction": "Direction",
|
||||
|
||||
@@ -14,12 +14,16 @@
|
||||
"status": "Status",
|
||||
"adminId": "Admin",
|
||||
"placeholderAdmin": "Select admin (optional)",
|
||||
"placeholderAdminTree": "Select admin by channel",
|
||||
"unassignedChannel": "Unassigned channel",
|
||||
"coin": "Coin",
|
||||
"placeholderCoinAdd": "Default 0 on create, read-only",
|
||||
"lotteryPoolConfig": "Lottery Pool Config",
|
||||
"placeholderLotteryPool": "Leave empty for custom weights below, or select pool",
|
||||
"currentConfig": "Current Config",
|
||||
"configLabelName": "Name",
|
||||
"configLabelPoolName": "Pool name",
|
||||
"configLabelCode": "Code",
|
||||
"configLabelType": "Type",
|
||||
"configLabelWeights": "T1–T5 Weights",
|
||||
"configLabelRemark": "Remark",
|
||||
@@ -44,7 +48,18 @@
|
||||
"ruleEnterCoin": "Please enter coin change",
|
||||
"ruleCoinPositive": "Coin change must be greater than 0",
|
||||
"ruleDeductExceed": "Deduct cannot exceed current balance",
|
||||
"operateSuccess": "Success"
|
||||
"operateSuccess": "Success",
|
||||
"addSuccess": "Added successfully",
|
||||
"editSuccess": "Updated successfully",
|
||||
"rulePasswordRequired": "Password is required",
|
||||
"ruleUsernameRequired": "Username is required",
|
||||
"ruleNicknameRequired": "Nickname is required",
|
||||
"rulePhoneRequired": "Phone is required",
|
||||
"ruleStatusRequired": "Status is required",
|
||||
"ruleCoinRequired": "Coin is required",
|
||||
"configTypeDefault": "Default",
|
||||
"configTypeKillScore": "Kill score",
|
||||
"configTypeUp": "Up score"
|
||||
},
|
||||
"search": {
|
||||
"username": "Username",
|
||||
@@ -57,7 +72,8 @@
|
||||
"placeholderNickname": "Please enter nickname",
|
||||
"placeholderPhoneFuzzy": "Phone (fuzzy)",
|
||||
"placeholderAll": "All",
|
||||
"exactSearch": "Exact"
|
||||
"exactSearch": "Exact",
|
||||
"createTime": "Register Time"
|
||||
},
|
||||
"table": {
|
||||
"username": "Username",
|
||||
|
||||
@@ -14,7 +14,12 @@
|
||||
"placeholderTotalDrawCount": "Auto sum",
|
||||
"placeholderRemark": "Remark (required)",
|
||||
"addSuccess": "Added successfully",
|
||||
"editSuccess": "Updated successfully"
|
||||
"editSuccess": "Updated successfully",
|
||||
"rulePlayerRequired": "Please select player",
|
||||
"ruleUseCoinsRequired": "Coins used is required",
|
||||
"rulePaidDrawRequired": "Paid draw count is required",
|
||||
"ruleFreeDrawRequired": "Free draw count is required",
|
||||
"ruleRemarkRequired": "Remark is required"
|
||||
},
|
||||
"search": {
|
||||
"player": "Player",
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
{
|
||||
"toolbar": {
|
||||
"coinChangeSummary": "Net Coin Change",
|
||||
"coinInflow": "Inflow",
|
||||
"coinOutflow": "Outflow"
|
||||
},
|
||||
"form": {
|
||||
"dialogTitleAdd": "Add Wallet Record",
|
||||
"dialogTitleEdit": "Edit Wallet Record",
|
||||
@@ -19,7 +24,10 @@
|
||||
"placeholderWalletAfter": "Auto calculated",
|
||||
"placeholderRemark": "Optional",
|
||||
"addSuccess": "Added successfully",
|
||||
"editSuccess": "Updated successfully"
|
||||
"editSuccess": "Updated successfully",
|
||||
"ruleUserRequired": "Please select user",
|
||||
"ruleCoinRequired": "Coin change is required",
|
||||
"ruleTypeRequired": "Please select type"
|
||||
},
|
||||
"search": {
|
||||
"type": "Type",
|
||||
|
||||
@@ -25,8 +25,6 @@
|
||||
"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)",
|
||||
"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)",
|
||||
"colGridNumber": "Points (grid_number)",
|
||||
"colDicePoints": "Dice Points",
|
||||
@@ -45,32 +43,40 @@
|
||||
},
|
||||
"weightEdit": {
|
||||
"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": {
|
||||
"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",
|
||||
"tabCounterclockwise": "Counter-clockwise"
|
||||
},
|
||||
"weightTest": {
|
||||
"title": "One-Click Weight Test",
|
||||
"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).",
|
||||
"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.",
|
||||
"labelKillModeEnabled": "Enable test kill mode",
|
||||
"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.",
|
||||
"killModePanelTitle": "Test kill mode",
|
||||
"killModeSwitchOn": "On",
|
||||
"killModeSwitchOff": "Off",
|
||||
"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",
|
||||
"sectionFreeAfterPlayAgain": "Free draw tier odds (after 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).",
|
||||
"sectionFreeAfterPlayAgain": "Free draws (play again)",
|
||||
"tierProbHintFreeChain": "Custom tiers: T1–T5 odds for free draws (combined with dice_reward row weights).",
|
||||
"stepPaid": "Paid ticket",
|
||||
"stepFree": "Free ticket",
|
||||
"labelLotteryTypePaid": "Test pool type",
|
||||
"labelLotteryTypeFree": "Test pool type",
|
||||
"labelLotteryTypePaid": "Paid tier pool",
|
||||
"labelLotteryTypeFree": "Free tier pool",
|
||||
"labelAnte": "Ante",
|
||||
"placeholderPaidPool": "Leave empty for custom tier odds below (default: default)",
|
||||
"placeholderFreePool": "Leave empty for custom tier odds below (default: killScore)",
|
||||
"placeholderAnte": "Select ante config",
|
||||
"anteRandomOption": "Random (each paid spin picks independently from channel ante configs)",
|
||||
"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%",
|
||||
"tierFieldLabel": "Tier {tier} (%)",
|
||||
"tierSumError": "Current sum of five tiers is {sum}%, cannot exceed 100%",
|
||||
@@ -81,7 +87,7 @@
|
||||
"btnNext": "Next",
|
||||
"btnStart": "Start test",
|
||||
"btnCancel": "Cancel",
|
||||
"warnAnte": "Ante must be greater than 0",
|
||||
"warnAnte": "Please select ante",
|
||||
"warnPaidSpins": "Paid clockwise + counter-clockwise spin counts must be greater than 0",
|
||||
"warnTestSafetyLine": "Test safety line must be greater than or equal to 0",
|
||||
"warnTotalSpins": "At least one of paid/free direction spin counts must be greater than 0",
|
||||
|
||||
@@ -8,6 +8,18 @@
|
||||
"tabIndex": "Reward Index",
|
||||
"tabBigwin": "Big Win Weights",
|
||||
"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.",
|
||||
"colId": "Index (id)",
|
||||
"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?",
|
||||
"confirmCreateRefOk": "Create",
|
||||
"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}",
|
||||
"createRefSuccessSkipped": "; {n} point(s) used fallback start index",
|
||||
"createRefSuccessSimple": "Created successfully",
|
||||
@@ -54,7 +79,7 @@
|
||||
"infoNoBigwin": "No BIGWIN rows. Set tier to BIGWIN in the Reward Index tab first.",
|
||||
"btnRuleGenerate": "Generate 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)",
|
||||
"ruleGenT2Row": "T2 (small win / break-even)",
|
||||
"ruleGenT3RealEvOnly": "T3 (rake)",
|
||||
@@ -64,11 +89,11 @@
|
||||
"ruleGenFixedCount": "Fixed count (CW & CCW)",
|
||||
"ruleGenRealEvStd": "real_ev standard",
|
||||
"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",
|
||||
"ruleGenInvalidT2RealEv": "T2 settlement amount must satisfy: 100 < value < 200",
|
||||
"ruleGenInvalidT3RealEv": "T3 settlement amount must satisfy: 0 < value < 100",
|
||||
"ruleGenInvalidT4RealEv": "T4 settlement amount must satisfy: value < 0",
|
||||
"ruleGenInvalidT5RealEv": "T5 “try again” real_ev must be 0",
|
||||
"ruleGenInvalidT1RealEv": "T1 (big prize) settlement must satisfy: value > 2",
|
||||
"ruleGenInvalidT2RealEv": "T2 (small win) settlement must satisfy: 1 < value ≤ 2",
|
||||
"ruleGenInvalidT3RealEv": "T3 (rake) settlement must satisfy: 0 < value ≤ 1",
|
||||
"ruleGenInvalidT4RealEv": "T4 (penalty) settlement must satisfy: value < 0",
|
||||
"ruleGenInvalidT5RealEv": "T5 (try again) settlement must be 0",
|
||||
"ruleGenT1Min": "T1 fixed count (CW & CCW)",
|
||||
"ruleGenT2Min": "T2 min (CW & CCW)",
|
||||
"ruleGenT4Max": "T4 fixed count (CW & CCW)",
|
||||
|
||||
@@ -17,6 +17,8 @@
|
||||
"chainModeNo": "No",
|
||||
"paidPlannedSpins": "Planned paid spins",
|
||||
"ante": "Ante",
|
||||
"anteRandom": "Random",
|
||||
"testSafetyLine": "Safety line",
|
||||
"playAgainCount": "Play-again count",
|
||||
"progressDraws": "{over} done",
|
||||
"progressFailed": "{over} before fail",
|
||||
@@ -51,11 +53,13 @@
|
||||
"testCountProgress": "In progress: {over} done",
|
||||
"testCountFailed": "{over} before failure",
|
||||
"chainModeLabel": "Chain play-again",
|
||||
"killModeOff": "Kill mode off",
|
||||
"paidPlannedSpins": "Planned paid spins",
|
||||
"testSafetyLine": "Test safety line",
|
||||
"createTime": "Created at",
|
||||
"admin": "Operator",
|
||||
"paidPoolId": "Paid lottery pool config ID",
|
||||
"freePoolId": "Free lottery pool config ID",
|
||||
"paidPoolId": "Paid lottery pool",
|
||||
"freePoolId": "Free lottery pool",
|
||||
"bigwinSnapshot": "BIGWIN weight snapshot",
|
||||
"sectionPaidTier": "Paid 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"
|
||||
}
|
||||
}
|
||||
@@ -1,38 +1,35 @@
|
||||
{
|
||||
"search": {
|
||||
"deptName": "channel(Department) Name",
|
||||
"deptCode": "Dept Code",
|
||||
"deptName": "Channel Name",
|
||||
"deptCode": "Channel Code",
|
||||
"status": "Status",
|
||||
"placeholderDeptName": "Please enter dept name",
|
||||
"placeholderDeptCode": "Please enter dept code",
|
||||
"placeholderDeptName": "Please enter channel name",
|
||||
"placeholderDeptCode": "Please enter channel code",
|
||||
"searchSelectPlaceholder": "Please select"
|
||||
},
|
||||
"table": {
|
||||
"deptName": "channel(Department) Name",
|
||||
"deptCode": "Dept Code",
|
||||
"leader": "Leader",
|
||||
"deptName": "Channel Name",
|
||||
"deptCode": "Channel Code",
|
||||
"leader": "Channel Leader",
|
||||
"sort": "Sort",
|
||||
"status": "Status",
|
||||
"createTime": "Create Time"
|
||||
},
|
||||
"form": {
|
||||
"titleAdd": "Add Department",
|
||||
"titleEdit": "Edit Department",
|
||||
"labelParentDept": "Parent Department",
|
||||
"labelDeptName": "Dept Name",
|
||||
"labelDeptCode": "Dept Code",
|
||||
"labelLeader": "Leader",
|
||||
"titleAdd": "Add Channel",
|
||||
"titleEdit": "Edit Channel",
|
||||
"labelDeptName": "Channel Name",
|
||||
"labelDeptCode": "Channel Code",
|
||||
"labelLeader": "Channel Leader",
|
||||
"labelRemark": "Description",
|
||||
"labelSort": "Sort",
|
||||
"labelStatus": "Enabled",
|
||||
"placeholderDeptName": "Please enter dept name",
|
||||
"placeholderDeptCode": "Please enter dept code",
|
||||
"placeholderDeptName": "Please enter channel name",
|
||||
"placeholderDeptCode": "Please enter channel code",
|
||||
"placeholderRemark": "Please enter description",
|
||||
"placeholderSort": "Please enter sort",
|
||||
"noParentDept": "No parent department",
|
||||
"ruleParentDeptRequired": "Please select parent department",
|
||||
"ruleDeptNameRequired": "Please enter dept name",
|
||||
"ruleDeptCodeRequired": "Please enter dept code",
|
||||
"ruleDeptNameRequired": "Please enter channel name",
|
||||
"ruleDeptCodeRequired": "Please enter channel code",
|
||||
"addSuccess": "Added successfully",
|
||||
"editSuccess": "Updated successfully"
|
||||
}
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
{
|
||||
"search": {
|
||||
"postName": "Post Name",
|
||||
"postCode": "Post Code",
|
||||
"status": "Status",
|
||||
"placeholderPostName": "Please enter post name",
|
||||
"placeholderPostCode": "Please enter post code",
|
||||
"searchSelectPlaceholder": "Please select"
|
||||
},
|
||||
"table": {
|
||||
"postName": "Post Name",
|
||||
"postCode": "Post Code",
|
||||
"sort": "Sort",
|
||||
"status": "Status",
|
||||
"createTime": "Create Time"
|
||||
},
|
||||
"form": {
|
||||
"titleAdd": "Add Post",
|
||||
"titleEdit": "Edit Post",
|
||||
"labelName": "Post Name",
|
||||
"labelCode": "Post Code",
|
||||
"labelRemark": "Description",
|
||||
"labelSort": "Sort",
|
||||
"labelStatus": "Enabled",
|
||||
"placeholderName": "Please enter post name",
|
||||
"placeholderCode": "Please enter post code",
|
||||
"placeholderRemark": "Please enter description",
|
||||
"placeholderSort": "Please enter sort",
|
||||
"ruleNameRequired": "Please enter post name",
|
||||
"ruleCodeRequired": "Please enter post code",
|
||||
"addSuccess": "Added successfully",
|
||||
"editSuccess": "Updated successfully"
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,7 @@
|
||||
"table": {
|
||||
"username": "Username",
|
||||
"phone": "Phone",
|
||||
"dept": "Department",
|
||||
"dept": "Channel",
|
||||
"dashboard": "Dashboard",
|
||||
"loginTime": "Last Login",
|
||||
"agentId": "Agent ID",
|
||||
@@ -28,9 +28,8 @@
|
||||
"labelPasswordConfirm": "Confirm Password",
|
||||
"labelEmail": "Email",
|
||||
"labelPhone": "Phone",
|
||||
"labelDept": "Department",
|
||||
"labelDept": "Channel",
|
||||
"labelRole": "Role",
|
||||
"labelPost": "Post",
|
||||
"labelGender": "Gender",
|
||||
"labelStatus": "Status",
|
||||
"labelRemark": "Remark",
|
||||
@@ -43,12 +42,15 @@
|
||||
"rulePasswordRequired": "Please enter password",
|
||||
"rulePasswordLength": "Length must be between 6 and 20 characters",
|
||||
"rulePasswordConfirmRequired": "Please enter confirm password",
|
||||
"ruleDeptRequired": "Please select department",
|
||||
"ruleDeptRequired": "Please select channel",
|
||||
"ruleRoleRequired": "Please select role",
|
||||
"addSuccess": "Added successfully",
|
||||
"editSuccess": "Updated successfully"
|
||||
},
|
||||
"ui": {
|
||||
"channelList": "Channel List",
|
||||
"viewingChannel": "Current channel",
|
||||
"defaultConfigTemplate": "Default config template",
|
||||
"promptNewPassword": "Please enter a new password",
|
||||
"passwordLengthError": "Password length must be between 6 and 16",
|
||||
"passwordChanged": "Password updated",
|
||||
|
||||
@@ -37,7 +37,16 @@
|
||||
"tips": "提示",
|
||||
"cancel": "取消",
|
||||
"confirm": "确定",
|
||||
"logOutTips": "您是否要退出登录?"
|
||||
"logOutTips": "您是否要退出登录?",
|
||||
"channelScope": {
|
||||
"listTitle": "渠道列表",
|
||||
"defaultTemplate": "默认配置模板",
|
||||
"defaultRoleTemplate": "默认角色模板",
|
||||
"allChannels": "全部",
|
||||
"currentConfig": "当前配置",
|
||||
"currentChannel": "当前渠道",
|
||||
"currentRole": "当前角色范围"
|
||||
}
|
||||
},
|
||||
"uiMsg": {
|
||||
"titlePrompt": "提示",
|
||||
@@ -291,23 +300,51 @@
|
||||
"gohome": "返回首页"
|
||||
},
|
||||
"console": {
|
||||
"filter": {
|
||||
"date": "统计日期",
|
||||
"placeholder": "选择日期",
|
||||
"today": "今日",
|
||||
"clear": "清空"
|
||||
},
|
||||
"card": {
|
||||
"playerRegister": "玩家注册",
|
||||
"playerCharge": "玩家充值",
|
||||
"playerWithdraw": "玩家提现",
|
||||
"playerPlayCount": "玩家游玩次数",
|
||||
"vsLastWeek": "较上周"
|
||||
"vsLastWeek": "较上周",
|
||||
"vsYesterday": "较昨日",
|
||||
"viewRechargeRecords": "查看充值记录",
|
||||
"viewWithdrawRecords": "查看提现记录",
|
||||
"viewPlayRecords": "查看游玩记录",
|
||||
"viewRegisterRecords": "查看注册玩家"
|
||||
},
|
||||
"nav": {
|
||||
"viewAllRecharge": "查看全部充值",
|
||||
"viewAllPlay": "查看全部游玩",
|
||||
"viewAllRegister": "查看全部注册"
|
||||
},
|
||||
"newPlayer": {
|
||||
"title": "新增玩家",
|
||||
"subtitle": "最新50条新增玩家记录",
|
||||
"subtitleByDate": "{date} 新增玩家记录(最多50条)",
|
||||
"player": "玩家",
|
||||
"balance": "余额",
|
||||
"ticket": "抽奖券"
|
||||
"ticket": "抽奖券",
|
||||
"registerTime": "注册时间"
|
||||
},
|
||||
"playRecord": {
|
||||
"title": "玩家游玩记录",
|
||||
"subtitle": "最新50条游玩记录",
|
||||
"subtitleByDate": "{date} 游玩记录(最多50条)",
|
||||
"player": "玩家",
|
||||
"reward": "中奖档位",
|
||||
"winCoin": "赢取平台币",
|
||||
"playTime": "游玩时间"
|
||||
},
|
||||
"walletRecord": {
|
||||
"title": "玩家充值记录",
|
||||
"subtitle": "最新50条充值记录",
|
||||
"subtitleByDate": "{date} 充值记录(最多50条)",
|
||||
"player": "玩家",
|
||||
"chargeAmount": "充值金额",
|
||||
"chargeTime": "充值时间"
|
||||
@@ -374,8 +411,7 @@
|
||||
"role": "角色管理",
|
||||
"userCenter": "个人中心",
|
||||
"menu": "菜单管理",
|
||||
"dept": "渠道(部门)管理",
|
||||
"post": "岗位管理",
|
||||
"dept": "渠道管理",
|
||||
"config": "系统配置"
|
||||
},
|
||||
"safeguard": {
|
||||
@@ -407,6 +443,9 @@
|
||||
"rewardConfigRecord": "权重测试记录",
|
||||
"playRecordTest": "抽奖记录(测试权重)",
|
||||
"config": "游戏配置"
|
||||
},
|
||||
"game": {
|
||||
"title": "游戏管理"
|
||||
}
|
||||
},
|
||||
"table": {
|
||||
@@ -426,6 +465,10 @@
|
||||
"max": "最大",
|
||||
"startTime": "开始时间",
|
||||
"endTime": "结束时间",
|
||||
"quickDate": "快捷日期",
|
||||
"quickToday": "今日",
|
||||
"quickYesterday": "昨日",
|
||||
"quickLast7Days": "近7天",
|
||||
"placeholderUsername": "请输入用户名",
|
||||
"placeholderNickname": "请输入昵称",
|
||||
"placeholderPhone": "请输入手机号",
|
||||
@@ -437,12 +480,10 @@
|
||||
"placeholderTaskName": "请输入任务名称",
|
||||
"placeholderTableName": "请输入数据表名称",
|
||||
"placeholderDataSource": "请输入数据源名称",
|
||||
"placeholderDeptName": "请输入部门名称",
|
||||
"placeholderDeptCode": "请输入部门编码",
|
||||
"placeholderDeptName": "请输入渠道名称",
|
||||
"placeholderDeptCode": "请输入渠道编码",
|
||||
"placeholderRoleName": "请输入角色名称",
|
||||
"placeholderRoleCode": "请输入角色编码",
|
||||
"placeholderPostName": "请输入岗位名称",
|
||||
"placeholderPostCode": "请输入岗位编码",
|
||||
"placeholderMenuName": "请输入菜单名称",
|
||||
"placeholderMenuRoute": "请输入菜单路由",
|
||||
"placeholderOperator": "请输入操作用户",
|
||||
@@ -506,15 +547,13 @@
|
||||
"system": {
|
||||
"username": "用户名",
|
||||
"phone": "手机号",
|
||||
"dept": "部门",
|
||||
"dept": "渠道",
|
||||
"dashboard": "首页",
|
||||
"loginTime": "上次登录",
|
||||
"agentId": "代理ID",
|
||||
"postName": "岗位名称",
|
||||
"postCode": "岗位编码",
|
||||
"deptName": "部门名称",
|
||||
"deptCode": "部门编码",
|
||||
"leader": "部门领导",
|
||||
"deptName": "渠道名称",
|
||||
"deptCode": "渠道编码",
|
||||
"leader": "渠道负责人",
|
||||
"roleName": "角色名称",
|
||||
"roleCode": "角色编码",
|
||||
"level": "角色级别",
|
||||
@@ -534,7 +573,7 @@
|
||||
"titleEn": "标题(英文)",
|
||||
"value": "值",
|
||||
"valueEn": "值(英文)",
|
||||
"noParentDept": "无上级部门",
|
||||
"noParentDept": "无上级渠道",
|
||||
"noParentMenu": "无上级菜单",
|
||||
"input": "文本框",
|
||||
"textarea": "文本域",
|
||||
|
||||
@@ -27,6 +27,8 @@
|
||||
"labelIsDefault": "默认底注",
|
||||
"placeholderName": "请输入名称",
|
||||
"placeholderTitle": "请输入标题",
|
||||
"placeholderNameAuto": "随底注倍率自动生成,如 x5",
|
||||
"placeholderTitleAuto": "随底注倍率自动生成,如 x5",
|
||||
"ruleNameRequired": "请输入名称",
|
||||
"ruleTitleRequired": "请输入标题",
|
||||
"ruleMultRequired": "请输入底注倍率",
|
||||
|
||||
68
saiadmin-artd/src/locales/langs/zh/dice/game.json
Normal file
@@ -0,0 +1,68 @@
|
||||
{
|
||||
"form": {
|
||||
"dialogTitleAdd": "新增游戏",
|
||||
"dialogTitleEdit": "编辑游戏",
|
||||
"provider": "供应商",
|
||||
"placeholderProvider": "请输入供应商名称",
|
||||
"providerCode": "供应商编码",
|
||||
"placeholderProviderCode": "请输入供应商编码",
|
||||
"gameCode": "游戏编号",
|
||||
"placeholderGameCode": "请输入游戏编号",
|
||||
"gameKey": "游戏唯一值",
|
||||
"placeholderGameKey": "请输入游戏唯一值",
|
||||
"gameName": "中文名称",
|
||||
"placeholderGameName": "请输入中文名称",
|
||||
"gameNameEn": "英文名称",
|
||||
"placeholderGameNameEn": "请输入英文名称",
|
||||
"gameType": "游戏类型",
|
||||
"placeholderGameType": "请输入游戏类型",
|
||||
"sort": "排序",
|
||||
"logo": "Logo地址",
|
||||
"tabPicker": "图片选择",
|
||||
"tabUpload": "图片上传",
|
||||
"gameUrl": "游戏地址",
|
||||
"placeholderGameUrl": "请输入游戏地址",
|
||||
"hallUrl": "大厅地址",
|
||||
"placeholderHallUrl": "请输入大厅地址",
|
||||
"status": "状态",
|
||||
"statusEnabled": "启用",
|
||||
"statusDisabled": "禁用",
|
||||
"remark": "备注",
|
||||
"placeholderRemark": "请输入备注",
|
||||
"addSuccess": "新增成功",
|
||||
"editSuccess": "更新成功",
|
||||
"ruleProviderRequired": "请输入供应商",
|
||||
"ruleProviderCodeRequired": "请输入供应商编码",
|
||||
"ruleGameCodeRequired": "请输入游戏编号",
|
||||
"ruleGameKeyRequired": "请输入游戏唯一值",
|
||||
"ruleGameNameRequired": "请输入中文名称",
|
||||
"ruleGameTypeRequired": "请输入游戏类型"
|
||||
},
|
||||
"search": {
|
||||
"providerCode": "供应商编码",
|
||||
"placeholderProviderCode": "请输入供应商编码",
|
||||
"gameCode": "游戏编号",
|
||||
"placeholderGameCode": "请输入游戏编号",
|
||||
"gameType": "游戏类型",
|
||||
"placeholderGameType": "请输入游戏类型",
|
||||
"status": "状态",
|
||||
"placeholderStatus": "请选择状态",
|
||||
"statusEnabled": "启用",
|
||||
"statusDisabled": "禁用"
|
||||
},
|
||||
"table": {
|
||||
"id": "ID",
|
||||
"provider": "供应商",
|
||||
"providerCode": "供应商编码",
|
||||
"gameCode": "游戏编号",
|
||||
"gameKey": "游戏唯一值",
|
||||
"gameName": "中文名",
|
||||
"gameNameEn": "英文名",
|
||||
"gameType": "类型",
|
||||
"sort": "排序",
|
||||
"status": "状态",
|
||||
"statusEnabled": "启用",
|
||||
"statusDisabled": "禁用",
|
||||
"updateTime": "更新时间"
|
||||
}
|
||||
}
|
||||
@@ -5,10 +5,14 @@
|
||||
"dialogTitleEdit": "编辑色子奖池配置",
|
||||
"placeholderName": "请输入名称",
|
||||
"placeholderRemark": "请输入备注",
|
||||
"placeholderPoolName": "请输入奖池名称,如:正常池",
|
||||
"placeholderConfigNote": "选填,用于说明该奖池用途或规则",
|
||||
"poolType": "奖池类型",
|
||||
"poolName": "奖池名称",
|
||||
"placeholderPoolType": "请选择奖池类型",
|
||||
"poolTypeNormal": "正常",
|
||||
"poolTypeKill": "强制杀猪",
|
||||
"poolTypeFree": "免费",
|
||||
"poolTypeKill": "强制杀分",
|
||||
"poolTypeT1": "T1高倍率",
|
||||
"safetyLine": "安全线",
|
||||
"t1Weight": "T1池权重(%)",
|
||||
@@ -21,19 +25,22 @@
|
||||
"currentPoolTitle": "当前彩金池",
|
||||
"loading": "加载中...",
|
||||
"poolName": "池子名称",
|
||||
"playerProfit": "玩家累计盈利(profit_amount):",
|
||||
"poolProfitAmount": "彩金池累计盈利(profit_amount):",
|
||||
"realtime": "实时",
|
||||
"profitCalcHint": "计算方式:付费每局按“赢取平台币 win_coin(含 BIGWIN)减去付费金额 压注金额paid_amount(= 压注倍数ante×1)”累加;免费每局按“玩家赢得平台币win_coin”累加。弹窗打开期间每 2 秒自动刷新",
|
||||
"tierRuleTitle": "抽奖档位规则",
|
||||
"tierRuleContent": "当玩家在当前彩金池的累计盈利 低于安全线 时,按 玩家 的 T*_weight 权重抽取档位;当累计盈利 高于或等于安全线 时,按 当前彩金池 的 T*_weight 权重抽取档位(杀分)。",
|
||||
"killScoreWeights": "杀分权重",
|
||||
"killWeightNote": "(杀分权重来自奖池配置,请在列表中编辑对应记录)",
|
||||
"btnResetProfit": "重置玩家累计盈利",
|
||||
"btnSaveSafetyLine": "保存安全线",
|
||||
"profitCalcHint": "累计在 name=default(正常)奖池上:付费每局 += win_coin − paid_amount(ante×1);免费每局 += win_coin。用于与安全线比较,判定付费抽奖是否切换杀分。弹窗打开期间每 2 秒自动刷新。",
|
||||
"tierRuleTitle": "付费抽奖档位规则",
|
||||
"tierRuleContent": "比较对象为 default 奖池的 profit_amount(非单个玩家盈利)。当 profit_amount 低于安全线或未开启杀分时,付费按玩家 T*_weight 抽档;当 profit_amount 高于或等于安全线且已开启杀分时,付费按 killScore 奖池抽档。免费抽奖始终按本渠道 name=free 奖池权重(无 free 时回退 default),与安全线无关。",
|
||||
"enableKillScore": "开启杀分",
|
||||
"killScoreWeights": "杀分权重(killScore)",
|
||||
"killWeightNote": "杀分权重请在列表中编辑 name=killScore(强制杀分)记录;本弹窗仅配置 default 奖池的安全线与杀分开关。",
|
||||
"btnResetProfit": "重置彩金池累计盈利",
|
||||
"btnSaveSafetyLine": "保存安全线与杀分开关",
|
||||
"safetyLineDefaultOnlyHint": "仅 name=default(正常)奖池的安全线参与杀分判定;其它奖池类型请勿在此配置安全线。",
|
||||
"safetyLineNotUsedReadonly": "当前奖池类型不参与杀分判定,安全线仅对「正常(default)」奖池生效,请通过「查看当前彩金池」或编辑正常行修改。",
|
||||
"ruleSafetyLineRequired": "请输入安全线",
|
||||
"msgGetPoolFailed": "获取彩金池失败",
|
||||
"msgSaveSuccess": "保存成功",
|
||||
"msgResetProfitSuccess": "玩家累计盈利已重置为 0",
|
||||
"msgResetProfitSuccess": "彩金池累计盈利已重置为 0",
|
||||
"msgResetFailed": "重置失败",
|
||||
"ruleNameRequired": "名称必需填写",
|
||||
"rulePoolTypeRequired": "请选择奖池类型",
|
||||
@@ -51,16 +58,22 @@
|
||||
},
|
||||
"search": {
|
||||
"poolType": "奖池类型",
|
||||
"poolName": "奖池名称",
|
||||
"placeholderName": "请输入名称",
|
||||
"placeholderPoolType": "请选择奖池类型",
|
||||
"poolTypeNormal": "正常",
|
||||
"poolTypeKill": "强制杀猪",
|
||||
"poolTypeFree": "免费",
|
||||
"poolTypeKill": "强制杀分",
|
||||
"poolTypeT1": "T1高倍率"
|
||||
},
|
||||
"table": {
|
||||
"name": "名称",
|
||||
"name": "内部标识",
|
||||
"poolName": "奖池名称",
|
||||
"configNote": "备注",
|
||||
"poolType": "奖池类型",
|
||||
"safetyLine": "安全线",
|
||||
"safetyLineNotUsed": "不参与杀分判定",
|
||||
"safetyLineTip": "仅「正常(default)」行有效",
|
||||
"t1PoolWeight": "T1池权重",
|
||||
"t2PoolWeight": "T2池权重",
|
||||
"t3PoolWeight": "T3池权重",
|
||||
|
||||
@@ -34,7 +34,15 @@
|
||||
"placeholderRewardTier": "请选择中奖档位",
|
||||
"addSuccess": "新增成功",
|
||||
"editSuccess": "修改成功",
|
||||
"validateFailed": "表单验证失败,请检查必填项与格式"
|
||||
"validateFailed": "表单验证失败,请检查必填项与格式",
|
||||
"rulePlayerRequired": "请选择玩家",
|
||||
"ruleLotteryConfigRequired": "请选择彩金池配置",
|
||||
"ruleLotteryTypeRequired": "请选择抽奖类型",
|
||||
"ruleIsWinRequired": "请选择是否中大奖",
|
||||
"ruleWinCoinRequired": "赢取平台币必填",
|
||||
"ruleRollArrayLength": "摇取点数必须为 5 个数",
|
||||
"ruleRollArrayValues": "摇取点数必须填写 5 个数,每个 1~6",
|
||||
"ruleRewardTierRequired": "请选择中奖档位"
|
||||
},
|
||||
"toolbar": {
|
||||
"platformTotalProfit": "平台总盈利"
|
||||
@@ -49,6 +57,7 @@
|
||||
"rollNumber": "摇取点数和",
|
||||
"rewardTier": "中奖档位",
|
||||
"rewardConfig": "奖励配置",
|
||||
"createTime": "创建时间",
|
||||
"usernameFuzzy": "用户名模糊",
|
||||
"nameFuzzy": "名称模糊",
|
||||
"uiTextFuzzy": "前端显示文本模糊",
|
||||
@@ -76,6 +85,7 @@
|
||||
"rollArray": "摇取点数",
|
||||
"rollNumber": "摇取点数和",
|
||||
"rewardTier": "中奖档位",
|
||||
"remark": "备注",
|
||||
"createTime": "创建时间",
|
||||
"updateTime": "更新时间"
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
"platformTotalProfit": "平台总盈利"
|
||||
},
|
||||
"search": {
|
||||
"lotteryPoolConfig": "彩金池配置",
|
||||
"placeholderLotteryPool": "请选择彩金池(可搜索 name)",
|
||||
"rewardConfigRecordId": "测试记录ID",
|
||||
"drawType": "抽奖类型",
|
||||
"direction": "方向",
|
||||
|
||||
@@ -14,12 +14,16 @@
|
||||
"status": "状态",
|
||||
"adminId": "所属管理员",
|
||||
"placeholderAdmin": "选择后台管理员(可选)",
|
||||
"placeholderAdminTree": "按渠道选择后台管理员",
|
||||
"unassignedChannel": "未分配渠道",
|
||||
"coin": "平台币",
|
||||
"placeholderCoinAdd": "创建时默认0,不可改",
|
||||
"lotteryPoolConfig": "彩金池配置",
|
||||
"placeholderLotteryPool": "留空则使用下方自定义权重,或选择彩金池",
|
||||
"currentConfig": "当前配置",
|
||||
"configLabelName": "名称",
|
||||
"configLabelPoolName": "奖池名称",
|
||||
"configLabelCode": "内部标识",
|
||||
"configLabelType": "类型",
|
||||
"configLabelWeights": "T1~T5 权重",
|
||||
"configLabelRemark": "备注",
|
||||
@@ -44,7 +48,18 @@
|
||||
"ruleEnterCoin": "请输入平台币变动",
|
||||
"ruleCoinPositive": "平台币变动必须大于 0",
|
||||
"ruleDeductExceed": "扣点不能超过当前余额",
|
||||
"operateSuccess": "操作成功"
|
||||
"operateSuccess": "操作成功",
|
||||
"addSuccess": "新增成功",
|
||||
"editSuccess": "修改成功",
|
||||
"rulePasswordRequired": "密码必需填写",
|
||||
"ruleUsernameRequired": "用户名必需填写",
|
||||
"ruleNicknameRequired": "昵称必需填写",
|
||||
"rulePhoneRequired": "手机号必需填写",
|
||||
"ruleStatusRequired": "状态必需填写",
|
||||
"ruleCoinRequired": "平台币必需填写",
|
||||
"configTypeDefault": "默认",
|
||||
"configTypeKillScore": "杀分",
|
||||
"configTypeUp": "上分"
|
||||
},
|
||||
"search": {
|
||||
"username": "用户名",
|
||||
@@ -57,7 +72,8 @@
|
||||
"placeholderNickname": "请输入昵称",
|
||||
"placeholderPhoneFuzzy": "手机号模糊查询",
|
||||
"placeholderAll": "全部",
|
||||
"exactSearch": "精确搜索"
|
||||
"exactSearch": "精确搜索",
|
||||
"createTime": "注册时间"
|
||||
},
|
||||
"table": {
|
||||
"username": "用户名",
|
||||
|
||||
@@ -14,7 +14,12 @@
|
||||
"placeholderTotalDrawCount": "自动求和",
|
||||
"placeholderRemark": "请输入备注(必填)",
|
||||
"addSuccess": "新增成功",
|
||||
"editSuccess": "修改成功"
|
||||
"editSuccess": "修改成功",
|
||||
"rulePlayerRequired": "请选择玩家",
|
||||
"ruleUseCoinsRequired": "消耗硬币必需填写",
|
||||
"rulePaidDrawRequired": "购买抽奖次数必需填写",
|
||||
"ruleFreeDrawRequired": "赠送抽奖次数必需填写",
|
||||
"ruleRemarkRequired": "备注必需填写"
|
||||
},
|
||||
"search": {
|
||||
"player": "玩家",
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
{
|
||||
"toolbar": {
|
||||
"coinChangeSummary": "平台币净变化",
|
||||
"coinInflow": "流入",
|
||||
"coinOutflow": "流出"
|
||||
},
|
||||
"form": {
|
||||
"dialogTitleAdd": "新增玩家钱包流水",
|
||||
"dialogTitleEdit": "编辑玩家钱包流水",
|
||||
@@ -19,7 +24,10 @@
|
||||
"placeholderWalletAfter": "根据平台币变化自动计算",
|
||||
"placeholderRemark": "选填",
|
||||
"addSuccess": "新增成功",
|
||||
"editSuccess": "修改成功"
|
||||
"editSuccess": "修改成功",
|
||||
"ruleUserRequired": "请选择用户",
|
||||
"ruleCoinRequired": "平台币变化必填",
|
||||
"ruleTypeRequired": "请选择类型"
|
||||
},
|
||||
"search": {
|
||||
"type": "类型",
|
||||
|
||||
@@ -25,8 +25,6 @@
|
||||
"emptyTier": "该档位暂无配置数据",
|
||||
"sumLineDual": "当前档位权重合计(顺时针):{cw};逆时针:{ccw}(各条 1-10000,档位内按权重比抽取,和不限制)",
|
||||
"sumLineSingle": "当前档位权重合计:{sum}(各条 1-10000,档位内按权重比抽取,和不限制)",
|
||||
"t4t5NoteSingle": "T4、T5 仅单一结果,无需配置权重。",
|
||||
"t4t5NoteDual": "T4、T5 档位抽中时仅有一个结果,无需配置权重。",
|
||||
"colEndIndexId": "结束索引(id)",
|
||||
"colGridNumber": "点数(grid_number)",
|
||||
"colDicePoints": "色子点数",
|
||||
@@ -45,32 +43,40 @@
|
||||
},
|
||||
"weightEdit": {
|
||||
"title": "奖励对照表(dice_reward)权重配比",
|
||||
"globalTip": "编辑的是奖励对照表(dice_reward / DiceReward 模型)的权重,按结束索引(end_index)区分顺时针与逆时针两套权重;抽奖时按当前方向取对应权重。"
|
||||
"globalTip": "编辑的是奖励对照表(dice_reward / DiceReward 模型)的权重,按结束索引(end_index)区分顺时针与逆时针两套权重;抽奖时按当前方向取对应权重,T4/T5 与 T1-T3 相同支持多点数权重配比。"
|
||||
},
|
||||
"weightRatio": {
|
||||
"title": "权重配比",
|
||||
"globalTip": "配置奖励对照表(dice_reward)的权重,一级按方向(顺时针/逆时针),二级按档位(T1-T5);各条权重 1-10000,档位内按权重比抽取。",
|
||||
"globalTip": "配置奖励对照表(dice_reward)的权重,一级按方向(顺时针/逆时针),二级按档位(T1-T5);各条权重 1-10000,档位内按权重比抽取骰子点数(T4/T5 与 T1-T3 逻辑相同,可配置多个点数的权重配比)。",
|
||||
"tabClockwise": "顺时针",
|
||||
"tabCounterclockwise": "逆时针"
|
||||
},
|
||||
"weightTest": {
|
||||
"title": "一键测试权重",
|
||||
"alertTitle": "彩金池逻辑说明",
|
||||
"alertBody": "测试模式默认不启用杀分切换;可通过下方“杀分开关 + 安全线”在测试内启用:当模拟玩家累计盈利达到安全线后,付费抽奖切换到 killScore。",
|
||||
"alertBody": "测试模式默认不启用杀分切换;开启下方「测试内杀分」后,可单独设置测试安全线(与彩金池配置中的安全线无关),用于模拟杀分触发。",
|
||||
"chainModeHint": "模拟方式:只配置付费抽奖次数(顺/逆时针)。付费抽到「再来一次」或 T5 时,下一局自动为免费抽奖,底注与触发局相同,抽奖类型记为免费、付费金额记为 0。免费抽奖的档位概率由下方「免费抽奖」配置决定(含通过再来一次触发的后续免费局)。",
|
||||
"killModeHint": "杀分开关开启后:以“模拟玩家累计盈利”作为判定值;当累计盈利 >= 安全线时,后续付费抽奖按 killScore 配置抽取;免费抽奖仍按“免费抽奖配置”执行。",
|
||||
"labelKillModeEnabled": "开启测试内杀分",
|
||||
"killModeHint": "杀分开关开启后:从 default 奖池当前 profit_amount 起步逐局累加(付费=win_coin-paid_amount,免费=win_coin);当累计盈利 ≥ 下方「测试安全线」时,后续付费抽奖切 killScore;免费抽奖仍走 name=free 奖池。",
|
||||
"killModePanelTitle": "测试内杀分",
|
||||
"killModeSwitchOn": "已开启",
|
||||
"killModeSwitchOff": "已关闭",
|
||||
"labelTestSafetyLine": "测试安全线",
|
||||
"testSafetyLineHint": "仅用于本次权重测试,与彩金池配置页的安全线独立;可设较小值以便快速观察杀分切换效果。",
|
||||
"poolProfitRef": "参考:当前 default 池盈利 {profit},彩金池配置安全线 {line}",
|
||||
"killModeOffHint": "关闭时全程按付费/免费配置抽档,不模拟杀分切换。",
|
||||
"sectionPaid": "付费抽奖",
|
||||
"sectionFreeAfterPlayAgain": "免费抽奖(再来一次后的档位概率)",
|
||||
"tierProbHintFreeChain": "当使用自定义档位时:以下为「免费抽奖」时 T1~T5 的档位概率(仅在有免费局时参与摇档,与 dice_reward 格子权重共同决定结果)。",
|
||||
"sectionFreeAfterPlayAgain": "免费抽奖(再来一次)",
|
||||
"tierProbHintFreeChain": "自定义档位时:免费局 T1~T5 档位概率(与 dice_reward 格子权重共同决定结果)。",
|
||||
"stepPaid": "付费抽奖券",
|
||||
"stepFree": "免费抽奖券",
|
||||
"labelLotteryTypePaid": "测试数据档位类型",
|
||||
"labelLotteryTypeFree": "测试数据档位类型",
|
||||
"labelAnte": "底注 ante",
|
||||
"placeholderPaidPool": "不选则下方自定义档位概率(默认 default)",
|
||||
"placeholderFreePool": "不选则下方自定义档位概率(默认 killScore)",
|
||||
"labelLotteryTypePaid": "付费档位奖池",
|
||||
"labelLotteryTypeFree": "免费档位奖池",
|
||||
"labelAnte": "底注",
|
||||
"placeholderAnte": "请选择底注配置",
|
||||
"anteRandomOption": "随机(每局付费抽奖从当前渠道底注配置中独立抽取)",
|
||||
"placeholderPaidPool": "不选则下方手动设定 T1–T5 档位权重",
|
||||
"placeholderFreePool": "不选则下方手动设定 T1–T5 档位权重",
|
||||
"selectedPoolHint": "已选奖池:{name}",
|
||||
"tierProbHint": "自定义档位概率(T1~T5),每档 0-100%,五档之和不能超过 100%",
|
||||
"tierFieldLabel": "档位 {tier}(%)",
|
||||
"tierSumError": "当前五档之和为 {sum}%,不能超过 100%",
|
||||
@@ -81,7 +87,7 @@
|
||||
"btnNext": "下一步",
|
||||
"btnStart": "开始测试",
|
||||
"btnCancel": "取消",
|
||||
"warnAnte": "底注 ante 必须大于 0",
|
||||
"warnAnte": "请选择底注",
|
||||
"warnPaidSpins": "付费抽奖顺时针与逆时针次数之和须大于 0",
|
||||
"warnTestSafetyLine": "测试安全线必须大于或等于 0",
|
||||
"warnTotalSpins": "付费或免费至少一种方向次数之和大于 0",
|
||||
|
||||
@@ -8,7 +8,19 @@
|
||||
"tabIndex": "奖励索引",
|
||||
"tabBigwin": "大奖权重",
|
||||
"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)",
|
||||
"colDicePoints": "色子点数",
|
||||
"colDisplayText": "显示文本",
|
||||
@@ -26,7 +38,7 @@
|
||||
"colBigwinPoints": "中大奖点数",
|
||||
"colDisplayInfo": "显示信息",
|
||||
"colDisplayInfoEn": "显示信息(英文)",
|
||||
"colRealPrize": "实际中奖",
|
||||
"colRealPrize": "结算金额",
|
||||
"colWeightRange": "权重(0-10000)",
|
||||
"placeholderDisplayInfoZh": "显示信息(中文)",
|
||||
"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 个点数、顺/逆时针分别生成。是否继续?",
|
||||
"confirmCreateRefOk": "确定创建",
|
||||
"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}",
|
||||
"createRefSuccessSkipped": ";{n} 个点数使用兜底起始索引",
|
||||
"createRefSuccessSimple": "创建成功",
|
||||
@@ -54,7 +79,7 @@
|
||||
"infoNoBigwin": "暂无 BIGWIN 档位配置,请先在「奖励索引」中设置 tier 为 BIGWIN",
|
||||
"btnRuleGenerate": "按规则生成",
|
||||
"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 大奖",
|
||||
"ruleGenT2Row": "T2 小赚/回本",
|
||||
"ruleGenT3RealEvOnly": "T3 抽水",
|
||||
@@ -64,11 +89,11 @@
|
||||
"ruleGenFixedCount": "固定条数(顺/逆)",
|
||||
"ruleGenRealEvStd": "结算金额",
|
||||
"ruleGenRealEvEditHint": "生成并保存后,仍可在本页表格中逐条修改显示文案、英文、真实结算与备注。",
|
||||
"ruleGenInvalidT1RealEv": "T1 的 结算金额 满足:200 < 值",
|
||||
"ruleGenInvalidT2RealEv": "T2 的 结算金额 满足:100 < 值 < 200",
|
||||
"ruleGenInvalidT3RealEv": "T3 的 结算金额 满足:0 < 值 < 100",
|
||||
"ruleGenInvalidT4RealEv": "T4 的 结算金额 满足:值 < 0",
|
||||
"ruleGenInvalidT5RealEv": "T5「再来一次」的 结算金额 须为 0",
|
||||
"ruleGenInvalidT1RealEv": "T1(大奖)结算金额须满足:值 > 2",
|
||||
"ruleGenInvalidT2RealEv": "T2(小赚)结算金额须满足:1 < 值 ≤ 2",
|
||||
"ruleGenInvalidT3RealEv": "T3(抽水)结算金额须满足:0 < 值 ≤ 1",
|
||||
"ruleGenInvalidT4RealEv": "T4(惩罚)结算金额须满足:值 < 0",
|
||||
"ruleGenInvalidT5RealEv": "T5(再来一次)结算金额须为 0",
|
||||
"ruleGenT1Min": "T1 固定条数(顺/逆)",
|
||||
"ruleGenT2Min": "T2 最少条数(顺/逆)",
|
||||
"ruleGenT4Max": "T4 固定条数(顺/逆)",
|
||||
|
||||
@@ -17,6 +17,8 @@
|
||||
"chainModeNo": "否",
|
||||
"paidPlannedSpins": "计划付费次数",
|
||||
"ante": "底注",
|
||||
"anteRandom": "随机",
|
||||
"testSafetyLine": "安全线",
|
||||
"playAgainCount": "再来一次次数",
|
||||
"progressDraws": "已完成 {over} 次",
|
||||
"progressFailed": "失败前 {over} 次",
|
||||
@@ -51,11 +53,13 @@
|
||||
"testCountProgress": "进行中:已完成 {over} 次",
|
||||
"testCountFailed": "失败前 {over} 次",
|
||||
"chainModeLabel": "链式再来一次",
|
||||
"killModeOff": "未开启杀分",
|
||||
"paidPlannedSpins": "计划付费次数",
|
||||
"testSafetyLine": "测试安全线",
|
||||
"createTime": "创建时间",
|
||||
"admin": "执行管理员",
|
||||
"paidPoolId": "付费奖池配置ID",
|
||||
"freePoolId": "免费奖池配置ID",
|
||||
"paidPoolId": "付费彩金池",
|
||||
"freePoolId": "免费彩金池",
|
||||
"bigwinSnapshot": "BIGWIN 权重快照",
|
||||
"sectionPaidTier": "付费抽奖档位概率(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": "请先点击编辑后再保存"
|
||||
}
|
||||
}
|
||||
@@ -1,38 +1,35 @@
|
||||
{
|
||||
"search": {
|
||||
"deptName": "渠道(部门)名称",
|
||||
"deptCode": "部门编码",
|
||||
"deptName": "渠道名称",
|
||||
"deptCode": "渠道编码",
|
||||
"status": "状态",
|
||||
"placeholderDeptName": "请输入部门名称",
|
||||
"placeholderDeptCode": "请输入部门编码",
|
||||
"placeholderDeptName": "请输入渠道名称",
|
||||
"placeholderDeptCode": "请输入渠道编码",
|
||||
"searchSelectPlaceholder": "请选择"
|
||||
},
|
||||
"table": {
|
||||
"deptName": "渠道(部门)名称",
|
||||
"deptCode": "部门编码",
|
||||
"leader": "部门领导",
|
||||
"deptName": "渠道名称",
|
||||
"deptCode": "渠道编码",
|
||||
"leader": "渠道负责人",
|
||||
"sort": "排序",
|
||||
"status": "状态",
|
||||
"createTime": "创建时间"
|
||||
},
|
||||
"form": {
|
||||
"titleAdd": "新增部门",
|
||||
"titleEdit": "编辑部门",
|
||||
"labelParentDept": "上级部门",
|
||||
"labelDeptName": "部门名称",
|
||||
"labelDeptCode": "部门编码",
|
||||
"labelLeader": "部门领导",
|
||||
"titleAdd": "新增渠道",
|
||||
"titleEdit": "编辑渠道",
|
||||
"labelDeptName": "渠道名称",
|
||||
"labelDeptCode": "渠道编码",
|
||||
"labelLeader": "渠道负责人",
|
||||
"labelRemark": "描述",
|
||||
"labelSort": "排序",
|
||||
"labelStatus": "启用",
|
||||
"placeholderDeptName": "请输入部门名称",
|
||||
"placeholderDeptCode": "请输入部门编码",
|
||||
"placeholderRemark": "请输入部门描述",
|
||||
"placeholderDeptName": "请输入渠道名称",
|
||||
"placeholderDeptCode": "请输入渠道编码",
|
||||
"placeholderRemark": "请输入渠道描述",
|
||||
"placeholderSort": "请输入排序",
|
||||
"noParentDept": "无上级部门",
|
||||
"ruleParentDeptRequired": "请选择上级部门",
|
||||
"ruleDeptNameRequired": "请输入部门名称",
|
||||
"ruleDeptCodeRequired": "请输入部门编码",
|
||||
"ruleDeptNameRequired": "请输入渠道名称",
|
||||
"ruleDeptCodeRequired": "请输入渠道编码",
|
||||
"addSuccess": "新增成功",
|
||||
"editSuccess": "修改成功"
|
||||
}
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
{
|
||||
"search": {
|
||||
"postName": "岗位名称",
|
||||
"postCode": "岗位编码",
|
||||
"status": "状态",
|
||||
"placeholderPostName": "请输入岗位名称",
|
||||
"placeholderPostCode": "请输入岗位编码",
|
||||
"searchSelectPlaceholder": "请选择"
|
||||
},
|
||||
"table": {
|
||||
"postName": "岗位名称",
|
||||
"postCode": "岗位编码",
|
||||
"sort": "排序",
|
||||
"status": "状态",
|
||||
"createTime": "创建时间"
|
||||
},
|
||||
"form": {
|
||||
"titleAdd": "新增岗位",
|
||||
"titleEdit": "编辑岗位",
|
||||
"labelName": "岗位名称",
|
||||
"labelCode": "岗位编码",
|
||||
"labelRemark": "描述",
|
||||
"labelSort": "排序",
|
||||
"labelStatus": "启用",
|
||||
"placeholderName": "请输入岗位名称",
|
||||
"placeholderCode": "请输入岗位编码",
|
||||
"placeholderRemark": "请输入岗位描述",
|
||||
"placeholderSort": "请输入排序",
|
||||
"ruleNameRequired": "请输入岗位名称",
|
||||
"ruleCodeRequired": "请输入岗位编码",
|
||||
"addSuccess": "新增成功",
|
||||
"editSuccess": "修改成功"
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,7 @@
|
||||
"table": {
|
||||
"username": "用户名",
|
||||
"phone": "手机号",
|
||||
"dept": "部门",
|
||||
"dept": "渠道",
|
||||
"dashboard": "首页",
|
||||
"loginTime": "上次登录",
|
||||
"agentId": "代理ID",
|
||||
@@ -28,9 +28,8 @@
|
||||
"labelPasswordConfirm": "确认密码",
|
||||
"labelEmail": "邮箱",
|
||||
"labelPhone": "手机号",
|
||||
"labelDept": "部门",
|
||||
"labelDept": "渠道",
|
||||
"labelRole": "角色",
|
||||
"labelPost": "岗位",
|
||||
"labelGender": "性别",
|
||||
"labelStatus": "状态",
|
||||
"labelRemark": "备注",
|
||||
@@ -43,12 +42,15 @@
|
||||
"rulePasswordRequired": "请输入密码",
|
||||
"rulePasswordLength": "长度在 6 到 20 个字符",
|
||||
"rulePasswordConfirmRequired": "请输入确认密码",
|
||||
"ruleDeptRequired": "请选择部门",
|
||||
"ruleDeptRequired": "请选择渠道",
|
||||
"ruleRoleRequired": "请选择角色",
|
||||
"addSuccess": "新增成功",
|
||||
"editSuccess": "修改成功"
|
||||
},
|
||||
"ui": {
|
||||
"channelList": "渠道列表",
|
||||
"viewingChannel": "当前配置渠道",
|
||||
"defaultConfigTemplate": "默认配置模板",
|
||||
"promptNewPassword": "请输入新密码",
|
||||
"passwordLengthError": "密码长度在6到16之间",
|
||||
"passwordChanged": "修改密码成功",
|
||||
|
||||
@@ -46,12 +46,21 @@ export async function loadPageLocale(routePath: string): Promise<void> {
|
||||
const modules = locale === LanguageEnum.EN ? enModules : zhModules
|
||||
|
||||
const tryPaths: string[] = [path]
|
||||
// 兼容别名路由:例如 /user 实际页面为 /system/user
|
||||
// 兼容别名路由:菜单 path 为短名但 locale 文件位于模块子目录
|
||||
// 例如:/user -> system/user,/game -> dice/game
|
||||
if (!path.includes('/')) {
|
||||
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
|
||||
|
||||
79
saiadmin-artd/src/utils/channelLayout.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
import type { RouteLocationNormalized } from 'vue-router'
|
||||
import { useUserStore } from '@/store/modules/user'
|
||||
|
||||
/** 页面自带左侧渠道栏,不再包一层全局渠道壳 */
|
||||
const BUILTIN_CHANNEL_LAYOUT_PATHS = [
|
||||
'/system/user',
|
||||
'/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 {
|
||||
const userStore = useUserStore()
|
||||
return Number(userStore.info?.id ?? 0) === 1
|
||||
}
|
||||
|
||||
/** 游戏配置类页面:显示「默认配置模板」 */
|
||||
export function isConfigChannelRoute(route: Pick<RouteLocationNormalized, 'path' | 'meta'>): boolean {
|
||||
if (route.meta?.channelScope === 'config') {
|
||||
return true
|
||||
}
|
||||
return /\/(config|ante_config|lottery_pool_config|reward_config|game)(\/|$)/.test(route.path)
|
||||
}
|
||||
|
||||
/** 角色管理:左侧渠道树含默认模板(dept_id=0)与各渠道角色 */
|
||||
export function isRoleChannelRoute(route: Pick<RouteLocationNormalized, 'path' | 'meta'>): boolean {
|
||||
if (route.meta?.channelScope === 'role') {
|
||||
return true
|
||||
}
|
||||
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 {
|
||||
if (!isSuperAdminUser()) {
|
||||
return false
|
||||
}
|
||||
if (route.meta?.isFullPage) {
|
||||
return false
|
||||
}
|
||||
const path = route.path
|
||||
if (isNoChannelLayoutRoute(route)) {
|
||||
return false
|
||||
}
|
||||
for (let i = 0; i < BUILTIN_CHANNEL_LAYOUT_PATHS.length; i++) {
|
||||
if (path.startsWith(BUILTIN_CHANNEL_LAYOUT_PATHS[i])) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
@@ -16,10 +16,11 @@
|
||||
*/
|
||||
import { AppRouteRecord } from '@/types/router'
|
||||
import { router } from '@/router'
|
||||
import { resolveAppAssetUrl } from '@/utils/navigation/resolveAppAssetUrl'
|
||||
|
||||
// 打开外部链接
|
||||
// 打开外部链接(含站内静态页,自动拼接部署 base)
|
||||
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}`
|
||||
}
|
||||
@@ -25,7 +25,6 @@ export const MAP_PATH_TO_MENU_I18N_KEY: Record<string, string> = {
|
||||
'/system/user-center': 'menus.system.userCenter',
|
||||
'/system/menu': 'menus.system.menu',
|
||||
'/system/dept': 'menus.system.dept',
|
||||
'/system/post': 'menus.system.post',
|
||||
'/system/config': 'menus.system.config',
|
||||
'/safeguard': 'menus.safeguard.title',
|
||||
'/safeguard/dict': 'menus.safeguard.dict',
|
||||
@@ -62,6 +61,10 @@ export const MAP_PATH_TO_MENU_I18N_KEY: Record<string, string> = {
|
||||
'/dice/play_record_test/index': 'menus.dice.playRecordTest',
|
||||
'/dice/config': '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/fail': 'menus.result.fail',
|
||||
'/exception/403': 'menus.exception.forbidden',
|
||||
|
||||
@@ -1,9 +1,25 @@
|
||||
<!-- 工作台页面 -->
|
||||
<!-- 工作台页面:大富翁色子游戏数据统计 -->
|
||||
<template>
|
||||
<div>
|
||||
<template v-if="userInfo.dashboard === 'statistics'">
|
||||
<CardList></CardList>
|
||||
<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 />
|
||||
|
||||
<template v-if="isStatisticsDashboard">
|
||||
<ElRow :gutter="20">
|
||||
<ElCol :sm="24" :md="12" :lg="10">
|
||||
<ActiveUser />
|
||||
@@ -12,30 +28,22 @@
|
||||
<SalesOverview />
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
</template>
|
||||
|
||||
<ElRow :gutter="20">
|
||||
<ElCol :sm="24" :md="12" :lg="12">
|
||||
<WalletRecordList />
|
||||
</ElCol>
|
||||
<ElCol :sm="24" :md="12" :lg="12">
|
||||
<NewPlayerList />
|
||||
</ElCol>
|
||||
<ElCol :sm="24" :md="12" :lg="12">
|
||||
<WalletRecordList />
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
</template>
|
||||
|
||||
<template v-if="userInfo.dashboard === 'work'">
|
||||
<ElRow :gutter="20">
|
||||
<ElCol :sm="24" :md="24" :lg="12">
|
||||
<NewUser />
|
||||
</ElCol>
|
||||
<ElCol :sm="24" :md="12" :lg="6">
|
||||
<Dynamic />
|
||||
</ElCol>
|
||||
<ElCol :sm="24" :md="12" :lg="6">
|
||||
<TodoList />
|
||||
<ElCol :sm="24" :md="24" :lg="24">
|
||||
<PlayRecordList />
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -45,17 +53,40 @@
|
||||
import SalesOverview from './modules/sales-overview.vue'
|
||||
import WalletRecordList from './modules/wallet-record-list.vue'
|
||||
import NewPlayerList from './modules/new-player-list.vue'
|
||||
import NewUser from './modules/new-user.vue'
|
||||
import Dynamic from './modules/dynamic-stats.vue'
|
||||
import TodoList from './modules/todo-list.vue'
|
||||
import PlayRecordList from './modules/play-record-list.vue'
|
||||
import { useCommon } from '@/hooks/core/useCommon'
|
||||
import { useUserStore } from '@/store/modules/user'
|
||||
import { getTodayDateString, provideDashboardScope } from '@/composables/useDashboardScope'
|
||||
|
||||
defineOptions({ name: 'Console' })
|
||||
|
||||
const userStore = useUserStore()
|
||||
const userInfo = userStore.getUserInfo
|
||||
const { selectedDate } = provideDashboardScope()
|
||||
|
||||
const resetToToday = () => {
|
||||
selectedDate.value = getTodayDateString()
|
||||
}
|
||||
|
||||
const clearDateFilter = () => {
|
||||
selectedDate.value = null
|
||||
}
|
||||
|
||||
/** 统计页额外展示充值图表 */
|
||||
const isStatisticsDashboard = computed(
|
||||
() => userStore.getUserInfo.dashboard === 'statistics'
|
||||
)
|
||||
|
||||
const { scrollToTop } = useCommon()
|
||||
scrollToTop()
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.dashboard-filter-bar {
|
||||
min-height: 52px;
|
||||
}
|
||||
|
||||
.dashboard-date-picker {
|
||||
width: 180px;
|
||||
max-width: 100%;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { fetchRechargeBarChart } from '@/api/dashboard'
|
||||
import { getChannelDeptRequestParams, useChannelDeptReload } from '@/composables/useChannelDeptScope'
|
||||
|
||||
/**
|
||||
* 充值金额数据
|
||||
@@ -29,10 +30,12 @@
|
||||
*/
|
||||
const xData = ref<string[]>([])
|
||||
|
||||
onMounted(async () => {
|
||||
fetchRechargeBarChart().then((data: any) => {
|
||||
const loadChart = () => {
|
||||
fetchRechargeBarChart(getChannelDeptRequestParams()).then((data: any) => {
|
||||
yData.value = data?.recharge_amount ?? []
|
||||
xData.value = data?.recharge_month ?? []
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
useChannelDeptReload(loadChart)
|
||||
</script>
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
<template>
|
||||
<ElRow :gutter="20" class="flex">
|
||||
<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>
|
||||
<ArtCountTo class="text-[26px] font-medium mt-2" :target="statData.player_count" :duration="1300" />
|
||||
<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
|
||||
class="ml-1 text-xs font-semibold"
|
||||
:class="changeClass(statData.player_count_change)"
|
||||
@@ -13,6 +16,9 @@
|
||||
{{ formatChange(statData.player_count_change) }}
|
||||
</span>
|
||||
</div>
|
||||
<ElButton type="primary" link class="dashboard-stat-link" @click.stop="goPlayerList">
|
||||
{{ $t('console.card.viewRegisterRecords') }}
|
||||
</ElButton>
|
||||
<div
|
||||
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>
|
||||
</ElCol>
|
||||
<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>
|
||||
<ArtCountTo
|
||||
class="text-[26px] font-medium mt-2"
|
||||
@@ -30,7 +39,7 @@
|
||||
:decimals="2"
|
||||
/>
|
||||
<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
|
||||
class="ml-1 text-xs font-semibold"
|
||||
:class="changeClass(statData.charge_amount_change)"
|
||||
@@ -38,6 +47,9 @@
|
||||
{{ formatChange(statData.charge_amount_change) }}
|
||||
</span>
|
||||
</div>
|
||||
<ElButton type="primary" link class="dashboard-stat-link" @click.stop="goWalletRecord(0)">
|
||||
{{ $t('console.card.viewRechargeRecords') }}
|
||||
</ElButton>
|
||||
<div
|
||||
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>
|
||||
</ElCol>
|
||||
<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>
|
||||
<ArtCountTo
|
||||
class="text-[26px] font-medium mt-2"
|
||||
@@ -55,7 +70,7 @@
|
||||
:decimals="2"
|
||||
/>
|
||||
<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
|
||||
class="ml-1 text-xs font-semibold"
|
||||
:class="changeClass(statData.withdraw_amount_change)"
|
||||
@@ -63,6 +78,9 @@
|
||||
{{ formatChange(statData.withdraw_amount_change) }}
|
||||
</span>
|
||||
</div>
|
||||
<ElButton type="primary" link class="dashboard-stat-link" @click.stop="goWalletRecord(1)">
|
||||
{{ $t('console.card.viewWithdrawRecords') }}
|
||||
</ElButton>
|
||||
<div
|
||||
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>
|
||||
</ElCol>
|
||||
<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>
|
||||
<ArtCountTo
|
||||
class="text-[26px] font-medium mt-2"
|
||||
@@ -79,7 +100,7 @@
|
||||
:duration="1300"
|
||||
/>
|
||||
<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
|
||||
class="ml-1 text-xs font-semibold"
|
||||
:class="changeClass(statData.play_count_change)"
|
||||
@@ -87,6 +108,9 @@
|
||||
{{ formatChange(statData.play_count_change) }}
|
||||
</span>
|
||||
</div>
|
||||
<ElButton type="primary" link class="dashboard-stat-link" @click.stop="goPlayRecord">
|
||||
{{ $t('console.card.viewPlayRecords') }}
|
||||
</ElButton>
|
||||
<div
|
||||
class="absolute top-0 bottom-0 right-5 m-auto size-12.5 rounded-xl flex-cc bg-theme/10"
|
||||
>
|
||||
@@ -99,6 +123,21 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { fetchStatistics } from '@/api/dashboard'
|
||||
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({
|
||||
player_count: 0,
|
||||
@@ -123,8 +162,8 @@
|
||||
return 'text-g-600'
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchStatistics().then((data: any) => {
|
||||
const loadStatistics = () => {
|
||||
fetchStatistics(queryParams.value).then((data: any) => {
|
||||
statData.value = {
|
||||
player_count: data?.player_count ?? 0,
|
||||
player_count_change: data?.player_count_change ?? 0,
|
||||
@@ -136,5 +175,41 @@
|
||||
play_count_change: data?.play_count_change ?? 0
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
useDashboardReload(loadStatistics)
|
||||
|
||||
const goWalletRecord = (type: number) => {
|
||||
openWalletRecord({
|
||||
type,
|
||||
...dashboardDateNavParams(selectedDate.value)
|
||||
})
|
||||
}
|
||||
|
||||
const goPlayRecord = () => {
|
||||
openPlayRecord(dashboardDateNavParams(selectedDate.value))
|
||||
}
|
||||
|
||||
const goPlayerList = () => {
|
||||
openPlayerList(dashboardDateNavParams(selectedDate.value))
|
||||
}
|
||||
</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>
|
||||
<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">
|
||||
<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>
|
||||
<ElButton type="primary" link @click="goPlayerList">
|
||||
{{ $t('console.nav.viewAllRegister') }}
|
||||
</ElButton>
|
||||
</div>
|
||||
<ArtTable
|
||||
class="w-full"
|
||||
@@ -17,11 +20,23 @@
|
||||
>
|
||||
<template #default>
|
||||
<ElTableColumn :label="$t('console.newPlayer.player')" prop="name" min-width="120" align="center" />
|
||||
<ElTableColumn :label="$t('console.newPlayer.balance')" prop="coin" min-width="120" align="center">
|
||||
<ElTableColumn :label="$t('console.newPlayer.balance')" prop="coin" min-width="100" align="center">
|
||||
<template #default="scope">
|
||||
{{ formatCoin(scope.row.coin) }}
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn
|
||||
:label="$t('console.newPlayer.ticket')"
|
||||
prop="total_ticket_count"
|
||||
min-width="90"
|
||||
align="center"
|
||||
/>
|
||||
<ElTableColumn
|
||||
:label="$t('console.newPlayer.registerTime')"
|
||||
prop="create_time"
|
||||
min-width="170"
|
||||
align="center"
|
||||
/>
|
||||
</template>
|
||||
</ArtTable>
|
||||
</div>
|
||||
@@ -29,17 +44,37 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { fetchNewPlayerList, type NewPlayerItem } from '@/api/dashboard'
|
||||
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 listSubtitle = computed(() =>
|
||||
hasDateFilter.value && selectedDate.value
|
||||
? t('console.newPlayer.subtitleByDate', { date: selectedDate.value })
|
||||
: t('console.newPlayer.subtitle')
|
||||
)
|
||||
|
||||
function formatCoin(val: number | undefined): string {
|
||||
if (val === undefined || val === null) return '0.00'
|
||||
return Number(val).toFixed(2)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchNewPlayerList().then((data) => {
|
||||
const loadList = () => {
|
||||
fetchNewPlayerList(queryParams.value).then((data) => {
|
||||
tableData.value = Array.isArray(data) ? data : []
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
useDashboardReload(loadList)
|
||||
|
||||
const goPlayerList = () => {
|
||||
openPlayerList(dashboardDateNavParams(selectedDate.value))
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
<template>
|
||||
<div class="art-card p-5 overflow-hidden mb-5 max-sm:mb-4">
|
||||
<div class="art-card-header mb-4 flex items-start justify-between gap-3">
|
||||
<div class="title">
|
||||
<h4>{{ $t('console.playRecord.title') }}</h4>
|
||||
<p class="text-g-600 text-sm mt-1">{{ listSubtitle }}</p>
|
||||
</div>
|
||||
<ElButton type="primary" link @click="goPlayRecords">
|
||||
{{ $t('console.nav.viewAllPlay') }}
|
||||
</ElButton>
|
||||
</div>
|
||||
<ArtTable
|
||||
class="w-full"
|
||||
:data="tableData"
|
||||
style="width: 100%"
|
||||
size="default"
|
||||
:border="false"
|
||||
:stripe="true"
|
||||
:header-cell-style="{ background: 'transparent' }"
|
||||
>
|
||||
<template #default>
|
||||
<ElTableColumn
|
||||
:label="$t('console.playRecord.player')"
|
||||
prop="player_name"
|
||||
min-width="120"
|
||||
align="center"
|
||||
/>
|
||||
<ElTableColumn
|
||||
:label="$t('console.playRecord.reward')"
|
||||
prop="reward_tier_label"
|
||||
min-width="140"
|
||||
align="center"
|
||||
/>
|
||||
<ElTableColumn
|
||||
:label="$t('console.playRecord.winCoin')"
|
||||
prop="win_coin"
|
||||
min-width="120"
|
||||
align="center"
|
||||
>
|
||||
<template #default="scope">
|
||||
{{ formatCoin(scope.row.win_coin) }}
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn
|
||||
:label="$t('console.playRecord.playTime')"
|
||||
prop="create_time"
|
||||
min-width="170"
|
||||
align="center"
|
||||
/>
|
||||
</template>
|
||||
</ArtTable>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { fetchPlayRecordList, type PlayRecordItem } from '@/api/dashboard'
|
||||
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 listSubtitle = computed(() =>
|
||||
hasDateFilter.value && selectedDate.value
|
||||
? t('console.playRecord.subtitleByDate', { date: selectedDate.value })
|
||||
: t('console.playRecord.subtitle')
|
||||
)
|
||||
|
||||
function formatCoin(val: number | undefined): string {
|
||||
if (val === undefined || val === null) return '0.00'
|
||||
return Number(val).toFixed(2)
|
||||
}
|
||||
|
||||
const loadList = () => {
|
||||
fetchPlayRecordList(queryParams.value).then((data) => {
|
||||
tableData.value = Array.isArray(data) ? data : []
|
||||
})
|
||||
}
|
||||
|
||||
useDashboardReload(loadList)
|
||||
|
||||
const goPlayRecords = () => {
|
||||
openPlayRecord(dashboardDateNavParams(selectedDate.value))
|
||||
}
|
||||
</script>
|
||||
@@ -17,6 +17,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { fetchRechargeChart } from '@/api/dashboard'
|
||||
import { getChannelDeptRequestParams, useChannelDeptReload } from '@/composables/useChannelDeptScope'
|
||||
|
||||
/**
|
||||
* 充值金额数据
|
||||
@@ -28,10 +29,12 @@
|
||||
*/
|
||||
const xData = ref<string[]>([])
|
||||
|
||||
onMounted(async () => {
|
||||
fetchRechargeChart().then((data: any) => {
|
||||
const loadChart = () => {
|
||||
fetchRechargeChart(getChannelDeptRequestParams()).then((data: any) => {
|
||||
yData.value = data?.recharge_amount ?? []
|
||||
xData.value = data?.recharge_date ?? []
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
useChannelDeptReload(loadChart)
|
||||
</script>
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
<template>
|
||||
<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">
|
||||
<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>
|
||||
<ElButton type="primary" link @click="goWalletRecords">
|
||||
{{ $t('console.nav.viewAllRecharge') }}
|
||||
</ElButton>
|
||||
</div>
|
||||
<ArtTable
|
||||
class="w-full"
|
||||
@@ -30,17 +33,40 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { fetchWalletRecordList, type WalletRecordItem } from '@/api/dashboard'
|
||||
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 listSubtitle = computed(() =>
|
||||
hasDateFilter.value && selectedDate.value
|
||||
? t('console.walletRecord.subtitleByDate', { date: selectedDate.value })
|
||||
: t('console.walletRecord.subtitle')
|
||||
)
|
||||
|
||||
function formatCoin(val: number | undefined): string {
|
||||
if (val === undefined || val === null) return '0.00'
|
||||
return Number(val).toFixed(2)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchWalletRecordList().then((data) => {
|
||||
const loadList = () => {
|
||||
fetchWalletRecordList(queryParams.value).then((data) => {
|
||||
tableData.value = Array.isArray(data) ? data : []
|
||||
})
|
||||
}
|
||||
|
||||
useDashboardReload(loadList)
|
||||
|
||||
const goWalletRecords = () => {
|
||||
openWalletRecord({
|
||||
type: 0,
|
||||
...dashboardDateNavParams(selectedDate.value)
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -109,6 +109,7 @@
|
||||
} = useTable({
|
||||
core: {
|
||||
apiFn: api.list,
|
||||
apiParams: { limit: 100 },
|
||||
columnsFactory: () => [
|
||||
{ type: 'selection' },
|
||||
{ prop: 'id', label: 'page.table.id', width: 80, align: 'center' },
|
||||
|
||||
@@ -9,13 +9,19 @@
|
||||
>
|
||||
<el-form ref="formRef" :model="formData" :rules="rules" label-width="120px">
|
||||
<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 :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 :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 :label="$t('page.form.labelIsDefault')" prop="is_default">
|
||||
<el-radio-group v-model="formData.is_default">
|
||||
@@ -36,6 +42,7 @@
|
||||
import { ElMessage } from 'element-plus'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { withChannelDeptParams } from '@/composables/useChannelDeptScope'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
@@ -86,6 +93,13 @@
|
||||
|
||||
const formData = reactive({ ...initialFormData })
|
||||
|
||||
function syncNameTitleFromMult() {
|
||||
const mult = Number(formData.mult) || 1
|
||||
const label = `x${mult}`
|
||||
formData.name = label
|
||||
formData.title = label
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
async (newVal) => {
|
||||
@@ -98,6 +112,7 @@
|
||||
if (typeof props.data.title === 'string') formData.title = props.data.title
|
||||
formData.mult = Number(props.data.mult ?? 1) || 1
|
||||
formData.is_default = Number(props.data.is_default ?? 0) === 1 ? 1 : 0
|
||||
syncNameTitleFromMult()
|
||||
}
|
||||
)
|
||||
|
||||
@@ -111,10 +126,10 @@
|
||||
try {
|
||||
await formRef.value.validate()
|
||||
if (props.dialogType === 'add') {
|
||||
await api.save(formData)
|
||||
await api.save(withChannelDeptParams(formData))
|
||||
ElMessage.success(t('page.form.addSuccess'))
|
||||
} else {
|
||||
await api.update(formData)
|
||||
await api.update(withChannelDeptParams(formData))
|
||||
ElMessage.success(t('page.form.editSuccess'))
|
||||
}
|
||||
emit('success')
|
||||
|
||||
@@ -36,5 +36,16 @@ export default {
|
||||
url: '/core/dice/ante_config/DiceAnteConfig/destroy',
|
||||
data: params
|
||||
})
|
||||
},
|
||||
|
||||
/** 底注下拉(按渠道) */
|
||||
async getOptions(params?: Record<string, unknown>) {
|
||||
const res = await request.get<
|
||||
Array<{ id: number; name: string; title: string; mult: number; is_default: number }>
|
||||
>({
|
||||
url: '/core/dice/ante_config/DiceAnteConfig/getOptions',
|
||||
params
|
||||
})
|
||||
return Array.isArray(res) ? res : []
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,29 @@
|
||||
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 接口
|
||||
@@ -20,31 +45,14 @@ export default {
|
||||
* 获取 DiceLotteryPoolConfig 列表数据,含 id、name、t1_weight~t5_weight,用于一键测试权重档位类型下拉
|
||||
* name 映射:default=原 type=0,killScore=原 type=1,up=原 type=2
|
||||
*/
|
||||
async getOptions(): Promise<
|
||||
Array<{
|
||||
id: number
|
||||
name: string
|
||||
t1_weight: number
|
||||
t2_weight: number
|
||||
t3_weight: number
|
||||
t4_weight: number
|
||||
t5_weight: number
|
||||
}>
|
||||
> {
|
||||
async getOptions(params?: Record<string, unknown>): Promise<LotteryPoolConfigOption[]> {
|
||||
const res = await request.get<any>({
|
||||
url: '/core/dice/lottery_pool_config/DiceLotteryPoolConfig/getOptions'
|
||||
url: '/core/dice/lottery_pool_config/DiceLotteryPoolConfig/getOptions',
|
||||
params
|
||||
})
|
||||
const rows = Array.isArray(res) ? res : (Array.isArray((res as any)?.data) ? (res as any).data : [])
|
||||
if (!Array.isArray(rows)) return []
|
||||
return rows.map((r: any) => ({
|
||||
id: Number(r.id),
|
||||
name: String(r.name ?? r.id ?? ''),
|
||||
t1_weight: Number(r.t1_weight ?? 0),
|
||||
t2_weight: Number(r.t2_weight ?? 0),
|
||||
t3_weight: Number(r.t3_weight ?? 0),
|
||||
t4_weight: Number(r.t4_weight ?? 0),
|
||||
t5_weight: Number(r.t5_weight ?? 0)
|
||||
}))
|
||||
return rows.map((r: Record<string, unknown>) => parseLotteryPoolConfigOption(r))
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -97,7 +105,7 @@ export default {
|
||||
/**
|
||||
* 获取当前彩金池(Redis 实例化,无则按 type=0 创建),含玩家累计盈利 profit_amount 实时值
|
||||
*/
|
||||
getCurrentPool() {
|
||||
getCurrentPool(params?: { dept_id?: number }) {
|
||||
return request.get<{
|
||||
id: number
|
||||
name: string
|
||||
@@ -110,14 +118,15 @@ export default {
|
||||
t5_weight: number
|
||||
profit_amount: number
|
||||
}>({
|
||||
url: '/core/dice/lottery_pool_config/DiceLotteryPoolConfig/getCurrentPool'
|
||||
url: '/core/dice/lottery_pool_config/DiceLotteryPoolConfig/getCurrentPool',
|
||||
params
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 更新当前彩金池:仅 safety_line、t1_weight~t5_weight,不可改 profit_amount
|
||||
*/
|
||||
updateCurrentPool(params: { safety_line?: number; kill_enabled?: number }) {
|
||||
updateCurrentPool(params: { safety_line?: number; kill_enabled?: number; dept_id?: number }) {
|
||||
return request.post<any>({
|
||||
url: '/core/dice/lottery_pool_config/DiceLotteryPoolConfig/updateCurrentPool',
|
||||
data: params
|
||||
@@ -127,9 +136,10 @@ export default {
|
||||
/**
|
||||
* 重置当前彩金池的玩家累计盈利(profit_amount 置为 0)
|
||||
*/
|
||||
resetProfitAmount() {
|
||||
resetProfitAmount(params?: { dept_id?: number }) {
|
||||
return request.post<any>({
|
||||
url: '/core/dice/lottery_pool_config/DiceLotteryPoolConfig/resetProfitAmount'
|
||||
url: '/core/dice/lottery_pool_config/DiceLotteryPoolConfig/resetProfitAmount',
|
||||
data: params || {}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import request from '@/utils/http'
|
||||
import { normalizeLotteryPoolOption, type LotteryPoolOption } from '@/views/plugin/dice/utils/lotteryPoolDisplay'
|
||||
|
||||
/**
|
||||
* 玩家抽奖记录 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
|
||||
@@ -64,16 +53,20 @@ export default {
|
||||
},
|
||||
|
||||
/** 获取玩家选项(id、username) */
|
||||
getPlayerOptions() {
|
||||
getPlayerOptions(params?: Record<string, unknown>) {
|
||||
return request.get<{ id: number; username: string }[]>({
|
||||
url: '/core/dice/play_record/DicePlayRecord/getPlayerOptions'
|
||||
url: '/core/dice/play_record/DicePlayRecord/getPlayerOptions',
|
||||
params
|
||||
})
|
||||
},
|
||||
|
||||
/** 获取彩金池配置选项(id、name) */
|
||||
getLotteryConfigOptions() {
|
||||
return request.get<{ id: number; name: string }[]>({
|
||||
url: '/core/dice/play_record/DicePlayRecord/getLotteryConfigOptions'
|
||||
/** 获取彩金池配置选项(含奖池名称) */
|
||||
async getLotteryConfigOptions(params?: Record<string, unknown>): Promise<LotteryPoolOption[]> {
|
||||
const res = await request.get<any>({
|
||||
url: '/core/dice/play_record/DicePlayRecord/getLotteryConfigOptions',
|
||||
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
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import request from '@/utils/http'
|
||||
import { normalizeLotteryPoolOption, type LotteryPoolOption } from '@/views/plugin/dice/utils/lotteryPoolDisplay'
|
||||
|
||||
/**
|
||||
* 大富翁-玩家 API接口
|
||||
@@ -84,26 +85,27 @@ export default {
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取彩金池配置选项(DiceLotteryPoolConfig.id、name),供 lottery_config_id 下拉使用
|
||||
* @returns [ { id, name } ]
|
||||
* 获取彩金池配置选项,供 lottery_config_id 下拉使用(含奖池名称 display_name)
|
||||
*/
|
||||
async getLotteryConfigOptions(): Promise<Array<{ id: number; name: string }>> {
|
||||
async getLotteryConfigOptions(params?: Record<string, unknown>): Promise<LotteryPoolOption[]> {
|
||||
const res = await request.get<any>({
|
||||
url: '/core/dice/player/DicePlayer/getLotteryConfigOptions'
|
||||
url: '/core/dice/player/DicePlayer/getLotteryConfigOptions',
|
||||
params
|
||||
})
|
||||
const rows = (Array.isArray(res) ? res : (res?.data ?? [])) as Array<{ id: number; name: string }>
|
||||
return rows.map((r) => ({ id: Number(r.id), name: String(r.name ?? r.id ?? '') }))
|
||||
const rows = (Array.isArray(res) ? res : (res?.data ?? [])) as Array<Record<string, unknown>>
|
||||
return rows.map((r) => normalizeLotteryPoolOption(r))
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取后台管理员选项(SystemUser),供 admin_id 下拉使用
|
||||
* @returns [ { id, username, realname, label } ]
|
||||
*/
|
||||
async getSystemUserOptions(): Promise<
|
||||
async getSystemUserOptions(params?: Record<string, unknown>): Promise<
|
||||
Array<{ id: number; username: string; realname: string; label: string }>
|
||||
> {
|
||||
const res = await request.get<any>({
|
||||
url: '/core/dice/player/DicePlayer/getSystemUserOptions'
|
||||
url: '/core/dice/player/DicePlayer/getSystemUserOptions',
|
||||
params
|
||||
})
|
||||
const rows = (Array.isArray(res) ? res : (res?.data ?? [])) as Array<{
|
||||
id: number
|
||||
@@ -117,5 +119,29 @@ export default {
|
||||
realname: String(r.realname ?? ''),
|
||||
label: String(r.label ?? r.username ?? r.id ?? '')
|
||||
}))
|
||||
},
|
||||
|
||||
/**
|
||||
* 超管:按渠道树状展示全部管理员;非超管:扁平列表
|
||||
*/
|
||||
async getSystemUserTreeOptions(params?: Record<string, unknown>): Promise<
|
||||
Array<{
|
||||
id: number | string
|
||||
label: string
|
||||
disabled?: boolean
|
||||
children?: Array<{ id: number; username: string; realname: string; label: string }>
|
||||
}>
|
||||
> {
|
||||
const res = await request.get<any>({
|
||||
url: '/core/dice/player/DicePlayer/getSystemUserTreeOptions',
|
||||
params
|
||||
})
|
||||
const rows = (Array.isArray(res) ? res : (res?.data ?? [])) as Array<{
|
||||
id: number | string
|
||||
label: string
|
||||
disabled?: boolean
|
||||
children?: Array<{ id: number; username: string; realname: string; label: string }>
|
||||
}>
|
||||
return rows
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -66,9 +54,10 @@ export default {
|
||||
/**
|
||||
* 获取玩家选项(id、username)用于下拉
|
||||
*/
|
||||
getPlayerOptions() {
|
||||
getPlayerOptions(params?: Record<string, unknown>) {
|
||||
return request.get<Api.Common.ApiData>({
|
||||
url: '/core/dice/player_ticket_record/DicePlayerTicketRecord/getPlayerOptions'
|
||||
url: '/core/dice/player_ticket_record/DicePlayerTicketRecord/getPlayerOptions',
|
||||
params
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,11 @@ export default {
|
||||
* @returns 数据列表
|
||||
*/
|
||||
list(params: Record<string, any>) {
|
||||
return request.get<Api.Common.ApiPage>({
|
||||
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',
|
||||
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
|
||||
@@ -66,9 +58,10 @@ export default {
|
||||
/**
|
||||
* 获取玩家选项(id、username)用于下拉
|
||||
*/
|
||||
getPlayerOptions() {
|
||||
getPlayerOptions(params?: Record<string, unknown>) {
|
||||
return request.get<{ id: number; username: string }[]>({
|
||||
url: '/core/dice/player_wallet_record/DicePlayerWalletRecord/getPlayerOptions'
|
||||
url: '/core/dice/player_wallet_record/DicePlayerWalletRecord/getPlayerOptions',
|
||||
params
|
||||
})
|
||||
},
|
||||
|
||||
|
||||
@@ -19,19 +19,20 @@ export default {
|
||||
* 权重编辑弹窗:按档位分组获取当前方向的配置+权重(单方向)
|
||||
* @param direction 0=顺时针 1=逆时针
|
||||
*/
|
||||
weightRatioList(direction: 0 | 1) {
|
||||
weightRatioList(direction: 0 | 1, params?: Record<string, unknown>) {
|
||||
return request.get<Api.Common.ApiData>({
|
||||
url: '/core/dice/reward/DiceReward/weightRatioList',
|
||||
params: { direction }
|
||||
params: { direction, ...(params || {}) }
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 权重编辑弹窗:按档位分组获取配置+顺时针/逆时针权重(dice_reward 双方向)
|
||||
*/
|
||||
weightRatioListWithDirection() {
|
||||
weightRatioListWithDirection(params?: Record<string, unknown>) {
|
||||
return request.get<Api.Common.ApiData>({
|
||||
url: '/core/dice/reward/DiceReward/weightRatioListWithDirection'
|
||||
url: '/core/dice/reward/DiceReward/weightRatioListWithDirection',
|
||||
params
|
||||
})
|
||||
},
|
||||
|
||||
@@ -39,10 +40,13 @@ export default {
|
||||
* 权重编辑弹窗:按 DiceReward 主键 id 批量更新 weight
|
||||
* @param items [{ id: DiceReward.id, weight: 1-10000 }, ...]
|
||||
*/
|
||||
batchUpdateWeights(items: Array<{ id: number; weight: number }>) {
|
||||
batchUpdateWeights(
|
||||
items: Array<{ id: number; weight: number }>,
|
||||
extra?: Record<string, unknown>
|
||||
) {
|
||||
return request.post<any>({
|
||||
url: '/core/dice/reward/DiceReward/batchUpdateWeights',
|
||||
data: { items }
|
||||
data: { items, ...(extra || {}) }
|
||||
})
|
||||
},
|
||||
|
||||
@@ -62,6 +66,7 @@ export default {
|
||||
*/
|
||||
startWeightTest(params: {
|
||||
ante?: number
|
||||
ante_config_id?: number
|
||||
lottery_config_id?: number
|
||||
paid_lottery_config_id?: number
|
||||
free_lottery_config_id?: number
|
||||
|
||||
@@ -66,19 +66,44 @@ export default {
|
||||
/**
|
||||
* 批量更新奖励索引配置(第一页:id、grid_number、ui_text、real_ev、tier、remark)
|
||||
*/
|
||||
batchUpdate(items: Array<{ id: number; grid_number?: number; ui_text?: string; real_ev?: number; tier?: string; remark?: string }>) {
|
||||
batchUpdate(
|
||||
items: Array<{ id: number; grid_number?: number; ui_text?: string; real_ev?: number; tier?: string; remark?: string }>,
|
||||
extra?: Record<string, any>
|
||||
) {
|
||||
return request.post<any>({
|
||||
url: '/core/dice/reward_config/DiceRewardConfig/batchUpdate',
|
||||
data: { items }
|
||||
data: { items, ...(extra || {}) }
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 按规则生成并保存奖励索引(需 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 权重配比:按档位分组获取配置列表
|
||||
*/
|
||||
weightRatioList() {
|
||||
weightRatioList(params?: Record<string, unknown>) {
|
||||
return request.get<Api.Common.ApiData>({
|
||||
url: '/core/dice/reward_config/DiceRewardConfig/weightRatioList'
|
||||
url: '/core/dice/reward_config/DiceRewardConfig/weightRatioList',
|
||||
params
|
||||
})
|
||||
},
|
||||
|
||||
@@ -86,34 +111,51 @@ export default {
|
||||
* T1-T5、BIGWIN 权重配比:批量更新顺时针/逆时针权重(写入 dice_reward)
|
||||
*/
|
||||
/** 按 DiceReward 主键 id 批量更新 weight;items: [{ id, weight }, ...] */
|
||||
batchUpdateWeights(items: Array<{ id: number; weight: number }>) {
|
||||
batchUpdateWeights(
|
||||
items: Array<{ id: number; weight: number }>,
|
||||
extra?: Record<string, unknown>
|
||||
) {
|
||||
return request.post<any>({
|
||||
url: '/core/dice/reward_config/DiceRewardConfig/batchUpdateWeights',
|
||||
data: { items }
|
||||
data: { items, ...(extra || {}) }
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 大奖权重:按 grid_number 批量保存 BIGWIN 权重(无需 reward id,不存在则自动创建)
|
||||
*/
|
||||
saveBigwinWeightsByGrid(items: Array<{ grid_number: number; weight: number }>) {
|
||||
saveBigwinWeightsByGrid(
|
||||
items: Array<{ grid_number: number; weight: number }>,
|
||||
extra?: Record<string, unknown>
|
||||
) {
|
||||
return request.post<any>({
|
||||
url: '/core/dice/reward_config/DiceRewardConfig/saveBigwinWeightsByGrid',
|
||||
data: { items }
|
||||
data: { items, ...(extra || {}) }
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 创建奖励对照:按当前奖励配置为顺时针(0)、逆时针(1)生成所有色子可能对应的 dice_reward 记录,权重默认 1,可在奖励对照页权重编辑中调整
|
||||
*/
|
||||
createRewardReference() {
|
||||
createRewardReference(params?: Record<string, any>) {
|
||||
return request.post<{
|
||||
created_clockwise: number
|
||||
created_counterclockwise: number
|
||||
updated_clockwise: number
|
||||
updated_counterclockwise: number
|
||||
}>({
|
||||
url: '/core/dice/reward_config/DiceRewardConfig/createRewardReference'
|
||||
url: '/core/dice/reward_config/DiceRewardConfig/createRewardReference',
|
||||
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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -107,6 +107,7 @@
|
||||
} = useTable({
|
||||
core: {
|
||||
apiFn: api.list,
|
||||
apiParams: { limit: 100 },
|
||||
columnsFactory: () => [
|
||||
// { type: 'selection' },
|
||||
{ prop: 'group', label: 'page.table.group', minWidth: 140, align: 'center' },
|
||||
|
||||
@@ -47,6 +47,7 @@
|
||||
import { ElMessage } from 'element-plus'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { withChannelDeptParams } from '@/composables/useChannelDeptScope'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
@@ -162,10 +163,10 @@
|
||||
try {
|
||||
await formRef.value.validate()
|
||||
if (props.dialogType === 'add') {
|
||||
await api.save(formData)
|
||||
await api.save(withChannelDeptParams(formData))
|
||||
ElMessage.success(t('page.form.saveSuccess'))
|
||||
} else {
|
||||
await api.update(formData)
|
||||
await api.update(withChannelDeptParams(formData))
|
||||
ElMessage.success(t('page.form.updateSuccess'))
|
||||
}
|
||||
emit('success')
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<template #icon>
|
||||
<ArtSvgIcon icon="ri:add-fill" />
|
||||
</template>
|
||||
新增
|
||||
{{ $t('table.actions.add') }}
|
||||
</ElButton>
|
||||
<ElButton
|
||||
v-permission="'dice:game:index:destroy'"
|
||||
@@ -21,7 +21,7 @@
|
||||
<template #icon>
|
||||
<ArtSvgIcon icon="ri:delete-bin-5-line" />
|
||||
</template>
|
||||
删除
|
||||
{{ $t('table.actions.delete') }}
|
||||
</ElButton>
|
||||
</ElSpace>
|
||||
</template>
|
||||
@@ -40,7 +40,9 @@
|
||||
@pagination:current-change="handleCurrentChange"
|
||||
>
|
||||
<template #status="{ row }">
|
||||
<ElTag :type="row.status === 1 ? 'success' : 'info'">{{ row.status === 1 ? '启用' : '禁用' }}</ElTag>
|
||||
<ElTag :type="row.status === 1 ? 'success' : 'info'">{{
|
||||
row.status === 1 ? $t('page.table.statusEnabled') : $t('page.table.statusDisabled')
|
||||
}}</ElTag>
|
||||
</template>
|
||||
<template #operation="{ row }">
|
||||
<div class="flex gap-2">
|
||||
@@ -103,22 +105,23 @@
|
||||
} = useTable({
|
||||
core: {
|
||||
apiFn: api.list,
|
||||
apiParams: { limit: 100 },
|
||||
columnsFactory: () => [
|
||||
{ type: 'selection', align: 'center' },
|
||||
{ prop: 'id', label: 'ID', width: 80, align: 'center' },
|
||||
{ prop: 'provider', label: '供应商', minWidth: 120, align: 'center' },
|
||||
{ prop: 'provider_code', label: '供应商编码', minWidth: 120, align: 'center' },
|
||||
{ prop: 'game_code', label: '游戏编号', minWidth: 120, align: 'center' },
|
||||
{ prop: 'game_key', label: '游戏唯一值', minWidth: 120, align: 'center' },
|
||||
{ prop: 'game_name', label: '中文名', minWidth: 120, align: 'center' },
|
||||
{ prop: 'game_name_en', label: '英文名', minWidth: 120, align: 'center' },
|
||||
{ prop: 'game_type', label: '类型', minWidth: 90, align: 'center' },
|
||||
{ prop: 'sort', label: '排序', width: 80, align: 'center' },
|
||||
{ prop: 'status', label: '状态', width: 90, align: 'center', useSlot: true },
|
||||
{ prop: 'update_time', label: '更新时间', minWidth: 160, align: 'center' },
|
||||
{ prop: 'id', label: 'page.table.id', width: 80, align: 'center' },
|
||||
{ prop: 'provider', label: 'page.table.provider', minWidth: 120, align: 'center' },
|
||||
{ prop: 'provider_code', label: 'page.table.providerCode', minWidth: 120, align: 'center' },
|
||||
{ prop: 'game_code', label: 'page.table.gameCode', minWidth: 120, align: 'center' },
|
||||
{ prop: 'game_key', label: 'page.table.gameKey', minWidth: 120, align: 'center' },
|
||||
{ prop: 'game_name', label: 'page.table.gameName', minWidth: 120, align: 'center' },
|
||||
{ prop: 'game_name_en', label: 'page.table.gameNameEn', minWidth: 120, align: 'center' },
|
||||
{ prop: 'game_type', label: 'page.table.gameType', minWidth: 90, align: 'center' },
|
||||
{ prop: 'sort', label: 'page.table.sort', width: 80, align: 'center' },
|
||||
{ prop: 'status', label: 'page.table.status', width: 90, align: 'center', useSlot: true },
|
||||
{ prop: 'update_time', label: 'page.table.updateTime', minWidth: 160, align: 'center' },
|
||||
{
|
||||
prop: 'operation',
|
||||
label: '操作',
|
||||
label: 'table.actions.operation',
|
||||
width: 110,
|
||||
align: 'center',
|
||||
fixed: 'right',
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="visible"
|
||||
:title="dialogType === 'add' ? '新增游戏' : '编辑游戏'"
|
||||
:title="dialogType === 'add' ? $t('page.form.dialogTitleAdd') : $t('page.form.dialogTitleEdit')"
|
||||
width="680px"
|
||||
align-center
|
||||
:close-on-click-modal="false"
|
||||
@@ -10,55 +10,55 @@
|
||||
<el-form ref="formRef" :model="formData" :rules="rules" label-width="120px">
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="供应商" prop="provider">
|
||||
<el-input v-model="formData.provider" placeholder="请输入供应商名称" />
|
||||
<el-form-item :label="$t('page.form.provider')" prop="provider">
|
||||
<el-input v-model="formData.provider" :placeholder="$t('page.form.placeholderProvider')" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="供应商编码" prop="provider_code">
|
||||
<el-input v-model="formData.provider_code" placeholder="请输入供应商编码" />
|
||||
<el-form-item :label="$t('page.form.providerCode')" prop="provider_code">
|
||||
<el-input v-model="formData.provider_code" :placeholder="$t('page.form.placeholderProviderCode')" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="游戏编号" prop="game_code">
|
||||
<el-input v-model="formData.game_code" placeholder="请输入游戏编号" />
|
||||
<el-form-item :label="$t('page.form.gameCode')" prop="game_code">
|
||||
<el-input v-model="formData.game_code" :placeholder="$t('page.form.placeholderGameCode')" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="游戏唯一值" prop="game_key">
|
||||
<el-input v-model="formData.game_key" placeholder="请输入游戏唯一值" />
|
||||
<el-form-item :label="$t('page.form.gameKey')" prop="game_key">
|
||||
<el-input v-model="formData.game_key" :placeholder="$t('page.form.placeholderGameKey')" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="中文名称" prop="game_name">
|
||||
<el-input v-model="formData.game_name" placeholder="请输入中文名称" />
|
||||
<el-form-item :label="$t('page.form.gameName')" prop="game_name">
|
||||
<el-input v-model="formData.game_name" :placeholder="$t('page.form.placeholderGameName')" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="英文名称" prop="game_name_en">
|
||||
<el-input v-model="formData.game_name_en" placeholder="请输入英文名称" />
|
||||
<el-form-item :label="$t('page.form.gameNameEn')" prop="game_name_en">
|
||||
<el-input v-model="formData.game_name_en" :placeholder="$t('page.form.placeholderGameNameEn')" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="游戏类型" prop="game_type">
|
||||
<el-input v-model="formData.game_type" placeholder="请输入游戏类型" />
|
||||
<el-form-item :label="$t('page.form.gameType')" prop="game_type">
|
||||
<el-input v-model="formData.game_type" :placeholder="$t('page.form.placeholderGameType')" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="排序" prop="sort">
|
||||
<el-form-item :label="$t('page.form.sort')" prop="sort">
|
||||
<el-input-number v-model="formData.sort" :min="1" :step="1" style="width: 100%" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-form-item label="Logo地址" prop="logo">
|
||||
<el-form-item :label="$t('page.form.logo')" prop="logo">
|
||||
<el-tabs v-model="logoInputMode" class="w-full">
|
||||
<el-tab-pane label="图片选择" name="picker">
|
||||
<el-tab-pane :label="$t('page.form.tabPicker')" name="picker">
|
||||
<sa-image-picker
|
||||
v-model="formData.logo"
|
||||
:multiple="false"
|
||||
@@ -67,7 +67,7 @@
|
||||
height="120px"
|
||||
/>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="图片上传" name="upload">
|
||||
<el-tab-pane :label="$t('page.form.tabUpload')" name="upload">
|
||||
<sa-image-upload
|
||||
v-model="formData.logo"
|
||||
:multiple="false"
|
||||
@@ -78,33 +78,42 @@
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</el-form-item>
|
||||
<el-form-item label="游戏地址" prop="game_url">
|
||||
<el-input v-model="formData.game_url" placeholder="请输入游戏地址" />
|
||||
<el-form-item :label="$t('page.form.gameUrl')" prop="game_url">
|
||||
<el-input v-model="formData.game_url" :placeholder="$t('page.form.placeholderGameUrl')" />
|
||||
</el-form-item>
|
||||
<el-form-item label="大厅地址" prop="hall_url">
|
||||
<el-input v-model="formData.hall_url" placeholder="请输入大厅地址" />
|
||||
<el-form-item :label="$t('page.form.hallUrl')" prop="hall_url">
|
||||
<el-input v-model="formData.hall_url" :placeholder="$t('page.form.placeholderHallUrl')" />
|
||||
</el-form-item>
|
||||
<el-form-item label="状态" prop="status">
|
||||
<el-form-item :label="$t('page.form.status')" prop="status">
|
||||
<el-radio-group v-model="formData.status">
|
||||
<el-radio :value="1">启用</el-radio>
|
||||
<el-radio :value="0">禁用</el-radio>
|
||||
<el-radio :value="1">{{ $t('page.form.statusEnabled') }}</el-radio>
|
||||
<el-radio :value="0">{{ $t('page.form.statusDisabled') }}</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="备注" prop="remark">
|
||||
<el-input v-model="formData.remark" type="textarea" :rows="2" placeholder="请输入备注" />
|
||||
<el-form-item :label="$t('page.form.remark')" prop="remark">
|
||||
<el-input
|
||||
v-model="formData.remark"
|
||||
type="textarea"
|
||||
:rows="2"
|
||||
:placeholder="$t('page.form.placeholderRemark')"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="handleClose">取消</el-button>
|
||||
<el-button type="primary" @click="handleSubmit">确定</el-button>
|
||||
<el-button @click="handleClose">{{ $t('common.cancel') }}</el-button>
|
||||
<el-button type="primary" @click="handleSubmit">{{ $t('table.form.submit') }}</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import api from '../../../api/game/index'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import { withChannelDeptParams } from '@/composables/useChannelDeptScope'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
interface Props {
|
||||
modelValue: boolean
|
||||
@@ -121,40 +130,24 @@
|
||||
data: undefined
|
||||
})
|
||||
const emit = defineEmits<Emits>()
|
||||
const formRef = ref<FormInstance>()
|
||||
const logoInputMode = ref<'picker' | 'upload'>('picker')
|
||||
|
||||
const visible = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (value) => emit('update:modelValue', value)
|
||||
set: (val) => emit('update:modelValue', val)
|
||||
})
|
||||
|
||||
type GameFormData = {
|
||||
id: number | null
|
||||
provider: string
|
||||
provider_code: string
|
||||
game_code: string
|
||||
game_key: string
|
||||
game_name: string
|
||||
game_name_en: string
|
||||
game_type: string
|
||||
logo: string
|
||||
game_url: string
|
||||
hall_url: string
|
||||
status: number
|
||||
sort: number
|
||||
remark: string
|
||||
}
|
||||
const formRef = ref<FormInstance>()
|
||||
const logoInputMode = ref('picker')
|
||||
|
||||
const initialFormData: GameFormData = {
|
||||
id: null,
|
||||
provider: 'Dicey Fun',
|
||||
provider_code: 'DF',
|
||||
const initialFormData = {
|
||||
id: undefined as number | undefined,
|
||||
provider: '',
|
||||
provider_code: '',
|
||||
game_code: '',
|
||||
game_key: '',
|
||||
game_name: '',
|
||||
game_name_en: '',
|
||||
game_type: 'slot',
|
||||
game_type: '',
|
||||
logo: '',
|
||||
game_url: '',
|
||||
hall_url: '',
|
||||
@@ -166,12 +159,12 @@
|
||||
const formData = reactive({ ...initialFormData })
|
||||
|
||||
const rules = computed<FormRules>(() => ({
|
||||
provider: [{ required: true, message: '请输入供应商', trigger: 'blur' }],
|
||||
provider_code: [{ required: true, message: '请输入供应商编码', trigger: 'blur' }],
|
||||
game_code: [{ required: true, message: '请输入游戏编号', trigger: 'blur' }],
|
||||
game_key: [{ required: true, message: '请输入游戏唯一值', trigger: 'blur' }],
|
||||
game_name: [{ required: true, message: '请输入中文名称', trigger: 'blur' }],
|
||||
game_type: [{ required: true, message: '请输入游戏类型', trigger: 'blur' }]
|
||||
provider: [{ required: true, message: t('page.form.ruleProviderRequired'), trigger: 'blur' }],
|
||||
provider_code: [{ required: true, message: t('page.form.ruleProviderCodeRequired'), trigger: 'blur' }],
|
||||
game_code: [{ required: true, message: t('page.form.ruleGameCodeRequired'), trigger: 'blur' }],
|
||||
game_key: [{ required: true, message: t('page.form.ruleGameKeyRequired'), trigger: 'blur' }],
|
||||
game_name: [{ required: true, message: t('page.form.ruleGameNameRequired'), trigger: 'blur' }],
|
||||
game_type: [{ required: true, message: t('page.form.ruleGameTypeRequired'), trigger: 'blur' }]
|
||||
}))
|
||||
|
||||
watch(
|
||||
@@ -209,11 +202,11 @@
|
||||
try {
|
||||
await formRef.value.validate()
|
||||
if (props.dialogType === 'add') {
|
||||
await api.save(formData)
|
||||
ElMessage.success('新增成功')
|
||||
await api.save(withChannelDeptParams(formData))
|
||||
ElMessage.success(t('page.form.addSuccess'))
|
||||
} else {
|
||||
await api.update(formData)
|
||||
ElMessage.success('更新成功')
|
||||
await api.update(withChannelDeptParams(formData))
|
||||
ElMessage.success(t('page.form.editSuccess'))
|
||||
}
|
||||
emit('success')
|
||||
handleClose()
|
||||
|
||||
@@ -8,25 +8,41 @@
|
||||
@search="handleSearch"
|
||||
>
|
||||
<el-col v-bind="setSpan(6)">
|
||||
<el-form-item label="供应商编码" prop="provider_code">
|
||||
<el-input v-model="formData.provider_code" placeholder="请输入供应商编码" clearable />
|
||||
<el-form-item :label="$t('page.search.providerCode')" prop="provider_code">
|
||||
<el-input
|
||||
v-model="formData.provider_code"
|
||||
:placeholder="$t('page.search.placeholderProviderCode')"
|
||||
clearable
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col v-bind="setSpan(6)">
|
||||
<el-form-item label="游戏编号" prop="game_code">
|
||||
<el-input v-model="formData.game_code" placeholder="请输入游戏编号" clearable />
|
||||
<el-form-item :label="$t('page.search.gameCode')" prop="game_code">
|
||||
<el-input
|
||||
v-model="formData.game_code"
|
||||
:placeholder="$t('page.search.placeholderGameCode')"
|
||||
clearable
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col v-bind="setSpan(6)">
|
||||
<el-form-item label="游戏类型" prop="game_type">
|
||||
<el-input v-model="formData.game_type" placeholder="请输入游戏类型" clearable />
|
||||
<el-form-item :label="$t('page.search.gameType')" prop="game_type">
|
||||
<el-input
|
||||
v-model="formData.game_type"
|
||||
:placeholder="$t('page.search.placeholderGameType')"
|
||||
clearable
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col v-bind="setSpan(6)">
|
||||
<el-form-item label="状态" prop="status">
|
||||
<el-select v-model="formData.status" placeholder="请选择状态" clearable>
|
||||
<el-option label="启用" :value="1" />
|
||||
<el-option label="禁用" :value="0" />
|
||||
<el-form-item :label="$t('page.search.status')" prop="status">
|
||||
<el-select
|
||||
v-model="formData.status"
|
||||
: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-form-item>
|
||||
</el-col>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<template>
|
||||
<div class="art-full-height">
|
||||
<!-- 搜索面板 -->
|
||||
<div class="flex-1 min-h-0 flex flex-col">
|
||||
<TableSearch v-model="searchForm" @search="handleSearch" @reset="resetSearchParams" />
|
||||
|
||||
<ElCard class="art-table-card" shadow="never">
|
||||
@@ -30,6 +29,12 @@
|
||||
@pagination:size-change="handleSizeChange"
|
||||
@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 }">
|
||||
<div class="flex gap-2">
|
||||
@@ -82,13 +87,10 @@
|
||||
getData()
|
||||
}
|
||||
|
||||
// 奖池类型展示:按 name 映射
|
||||
const typeFormatter = (row: Record<string, unknown>) => {
|
||||
const n = String(row.name ?? '')
|
||||
if (n === 'default') return t('page.search.poolTypeNormal')
|
||||
if (n === 'killScore') return t('page.search.poolTypeKill')
|
||||
if (n === 'up') return t('page.search.poolTypeT1')
|
||||
return n || '-'
|
||||
const poolNameFormatter = (row: Record<string, unknown>) => {
|
||||
const remark = String(row.remark ?? '').trim()
|
||||
if (remark) return remark
|
||||
return String(row.name ?? '').trim() || '-'
|
||||
}
|
||||
|
||||
// 权重列带 %
|
||||
@@ -97,6 +99,17 @@
|
||||
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 {
|
||||
columns,
|
||||
@@ -115,9 +128,26 @@
|
||||
core: {
|
||||
apiFn: api.list,
|
||||
columnsFactory: () => [
|
||||
{ prop: 'name', label: 'page.table.name', align: 'center' },
|
||||
{ prop: 'name', label: 'page.table.poolType', width: 100, align: 'center', formatter: typeFormatter },
|
||||
{ prop: 'safety_line', label: 'page.table.safetyLine', align: 'center' },
|
||||
{ prop: 'remark', label: 'page.table.poolName', minWidth: 120, align: 'center', formatter: poolNameFormatter },
|
||||
{
|
||||
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',
|
||||
label: 'page.table.t1PoolWeight',
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
</div>
|
||||
<div class="profit-row mb-3">
|
||||
<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">{{
|
||||
displayProfitAmount
|
||||
}}</span>
|
||||
@@ -41,8 +41,9 @@
|
||||
:precision="2"
|
||||
style="width: 100%"
|
||||
/>
|
||||
<div class="text-gray-500 text-xs mt-1">{{ $t('page.table.safetyLineTip') }}</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="开启杀分">
|
||||
<el-form-item :label="$t('page.form.enableKillScore')">
|
||||
<el-switch v-model="formData.kill_enabled" :active-value="1" :inactive-value="0" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('page.form.killScoreWeights')">
|
||||
@@ -85,8 +86,13 @@
|
||||
import { ElMessage } from 'element-plus'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import {
|
||||
getChannelDeptRequestParams,
|
||||
withChannelDeptParams
|
||||
} from '@/composables/useChannelDeptScope'
|
||||
|
||||
const { t } = useI18n()
|
||||
const channelDeptParams = () => getChannelDeptRequestParams()
|
||||
|
||||
interface PoolData {
|
||||
id: number
|
||||
@@ -151,7 +157,7 @@
|
||||
if (!visible.value) return
|
||||
try {
|
||||
loading.value = true
|
||||
const res = await api.getCurrentPool()
|
||||
const res = await api.getCurrentPool(channelDeptParams())
|
||||
const data = res as unknown as PoolData
|
||||
if (data && typeof data === 'object') {
|
||||
pool.value = data
|
||||
@@ -172,7 +178,7 @@
|
||||
stopPolling()
|
||||
return
|
||||
}
|
||||
api.getCurrentPool().then((res) => {
|
||||
api.getCurrentPool(channelDeptParams()).then((res) => {
|
||||
const data = res as unknown as PoolData
|
||||
if (pool.value && data && typeof data === 'object' && data.profit_amount != null) {
|
||||
pool.value.profit_amount = data.profit_amount
|
||||
@@ -193,10 +199,12 @@
|
||||
try {
|
||||
await formRef.value?.validate?.()
|
||||
saving.value = true
|
||||
await api.updateCurrentPool({
|
||||
await api.updateCurrentPool(
|
||||
withChannelDeptParams({
|
||||
safety_line: formData.safety_line,
|
||||
kill_enabled: formData.kill_enabled
|
||||
})
|
||||
)
|
||||
ElMessage.success(t('page.form.msgSaveSuccess'))
|
||||
await loadPool()
|
||||
emit('success')
|
||||
@@ -211,7 +219,7 @@
|
||||
if (!pool.value) return
|
||||
try {
|
||||
resetting.value = true
|
||||
await api.resetProfitAmount()
|
||||
await api.resetProfitAmount(channelDeptParams())
|
||||
ElMessage.success(t('page.form.msgResetProfitSuccess'))
|
||||
await loadPool()
|
||||
emit('success')
|
||||
@@ -232,6 +240,8 @@
|
||||
() => props.modelValue,
|
||||
(open) => {
|
||||
if (open) {
|
||||
// 切换渠道后再打开弹窗时,先清掉旧池数据,避免视觉残留
|
||||
pool.value = null
|
||||
loadPool().then(() => startPolling())
|
||||
} else {
|
||||
stopPolling()
|
||||
|
||||
@@ -15,24 +15,41 @@
|
||||
:disabled="dialogType === 'edit'"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('form.labelRemark')" prop="remark">
|
||||
<el-form-item :label="$t('page.form.poolName')" prop="remark">
|
||||
<el-input
|
||||
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"
|
||||
:rows="3"
|
||||
:placeholder="$t('page.form.placeholderRemark')"
|
||||
:placeholder="$t('page.form.placeholderConfigNote')"
|
||||
maxlength="500"
|
||||
show-word-limit
|
||||
/>
|
||||
</el-form-item>
|
||||
<!-- 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
|
||||
v-model="formData.safety_line"
|
||||
:min="0"
|
||||
:precision="2"
|
||||
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 :label="$t('page.form.t1Weight')" prop="t1_weight">
|
||||
<el-slider v-model="formData.t1_weight" :min="0" :max="100" :step="0.01" show-input />
|
||||
@@ -70,6 +87,7 @@
|
||||
import { ElMessage } from 'element-plus'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useInjectedChannelDept, withChannelDeptParams } from '@/composables/useChannelDeptScope'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
@@ -110,25 +128,46 @@
|
||||
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' }],
|
||||
t1_weight: [{ required: true, message: t('page.form.ruleT1Required'), trigger: 'blur' }],
|
||||
t2_weight: [{ required: true, message: t('page.form.ruleT2Required'), trigger: 'blur' }],
|
||||
t3_weight: [{ required: true, message: t('page.form.ruleT3Required'), trigger: 'blur' }],
|
||||
t4_weight: [{ required: true, message: t('page.form.ruleT4Required'), 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
|
||||
})
|
||||
|
||||
/**
|
||||
* 初始数据(权重为数字便于输入与校验)
|
||||
*/
|
||||
const initialFormData = {
|
||||
id: null as number | null,
|
||||
dept_id: undefined as number | undefined,
|
||||
name: '',
|
||||
remark: '',
|
||||
config_note: '',
|
||||
safety_line: 0 as number,
|
||||
t1_weight: 0 as number,
|
||||
t2_weight: 0 as number,
|
||||
@@ -174,6 +213,7 @@
|
||||
if (!props.data) return
|
||||
const numKeys = [
|
||||
'id',
|
||||
'dept_id',
|
||||
'safety_line',
|
||||
't1_weight',
|
||||
't2_weight',
|
||||
@@ -204,6 +244,8 @@
|
||||
/**
|
||||
* 提交表单
|
||||
*/
|
||||
const channelScope = useInjectedChannelDept()
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!formRef.value) return
|
||||
try {
|
||||
@@ -212,11 +254,22 @@
|
||||
ElMessage.warning(t('page.form.msgWeightsMust100'))
|
||||
return
|
||||
}
|
||||
const submitData = withChannelDeptParams({
|
||||
...formData,
|
||||
dept_id:
|
||||
formData.dept_id ??
|
||||
props.data?.dept_id ??
|
||||
channelScope?.selectedDeptId.value
|
||||
})
|
||||
const { safety_line, ...submitWithoutSafetyLine } = submitData
|
||||
const payload = isDefaultPoolName(submitData.name)
|
||||
? submitData
|
||||
: submitWithoutSafetyLine
|
||||
if (props.dialogType === 'add') {
|
||||
await api.save(formData)
|
||||
await api.save(payload)
|
||||
ElMessage.success(t('page.form.msgAddSuccess'))
|
||||
} else {
|
||||
await api.update(formData)
|
||||
await api.update(payload)
|
||||
ElMessage.success(t('page.form.msgUpdateSuccess'))
|
||||
}
|
||||
emit('success')
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
<template>
|
||||
<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">
|
||||
<!-- 表格头部 -->
|
||||
@@ -138,12 +139,24 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { useTable } from '@/hooks/core/useTable'
|
||||
import { useSaiAdmin } from '@/composables/useSaiAdmin'
|
||||
import api from '../../api/play_record/index'
|
||||
import TableSearch from './modules/table-search.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 route = useRoute()
|
||||
const routeInit = getRecordRouteInit(route.query)
|
||||
const activeDatePreset = ref<DatePresetKey | null>(routeInit.activePreset)
|
||||
|
||||
// 搜索表单
|
||||
const searchForm = ref<Record<string, unknown>>({
|
||||
@@ -157,9 +170,40 @@
|
||||
roll_number_max: undefined,
|
||||
reward_ui_text: 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 求和 - 玩家总收益) */
|
||||
const totalWinCoin = ref<number | null>(null)
|
||||
|
||||
@@ -169,16 +213,37 @@
|
||||
return res
|
||||
}
|
||||
|
||||
// 搜索处理
|
||||
const handleSearch = (params: Record<string, any>) => {
|
||||
Object.assign(searchParams, params)
|
||||
applySearchParams(params)
|
||||
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>) =>
|
||||
row?.dicePlayer?.username ?? row?.player_id ?? '-'
|
||||
const lotteryConfigNameFormatter = (row: Record<string, any>) =>
|
||||
row?.diceLotteryPoolConfig?.name ?? row?.lottery_config_id ?? '-'
|
||||
const lotteryConfigNameFormatter = (row: Record<string, any>) => lotteryPoolRowLabel(row)
|
||||
const rewardTierFormatter = (row: Record<string, any>) => row?.reward_tier ?? '-'
|
||||
|
||||
/** 摇取点数格式化为 1,3,4,5,6,6 */
|
||||
@@ -221,6 +286,8 @@
|
||||
core: {
|
||||
apiFn: listApi,
|
||||
apiParams: { limit: 100 },
|
||||
excludeParams: ['create_time'],
|
||||
immediate: !routeInit.hasFilter,
|
||||
columnsFactory: () => [
|
||||
// { type: 'selection' },
|
||||
{ prop: 'id', label: 'page.table.id', width: 80 },
|
||||
@@ -253,6 +320,13 @@
|
||||
width: 100,
|
||||
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: 'update_time', label: 'page.table.updateTime', width: 170 },
|
||||
{
|
||||
@@ -277,6 +351,12 @@
|
||||
handleSelectionChange
|
||||
// selectedRows
|
||||
} = useSaiAdmin()
|
||||
|
||||
useRecordRouteSync({
|
||||
searchForm,
|
||||
activeDatePreset,
|
||||
onSearch: handleSearch
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="visible"
|
||||
:title="dialogType === 'add' ? $t('page.form.dialogTitleAdd') : $t('page.form.dialogTitleEdit')"
|
||||
:title="$t('page.form.dialogTitleEdit')"
|
||||
width="600px"
|
||||
align-center
|
||||
:close-on-click-modal="false"
|
||||
@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-select
|
||||
v-model="formData.player_id"
|
||||
@@ -15,7 +15,7 @@
|
||||
clearable
|
||||
filterable
|
||||
style="width: 100%"
|
||||
:disabled="dialogType === 'edit'"
|
||||
disabled
|
||||
>
|
||||
<el-option
|
||||
v-for="item in playerOptions"
|
||||
@@ -32,12 +32,12 @@
|
||||
clearable
|
||||
filterable
|
||||
style="width: 100%"
|
||||
:disabled="dialogType === 'edit'"
|
||||
disabled
|
||||
>
|
||||
<el-option
|
||||
v-for="item in lotteryConfigOptions"
|
||||
:key="item.id"
|
||||
:label="item.name"
|
||||
:label="lotteryPoolOptionLabel(item)"
|
||||
:value="item.id"
|
||||
/>
|
||||
</el-select>
|
||||
@@ -48,7 +48,7 @@
|
||||
:placeholder="$t('form.placeholderSelect')"
|
||||
clearable
|
||||
style="width: 100%"
|
||||
:disabled="dialogType === 'edit'"
|
||||
disabled
|
||||
>
|
||||
<el-option :label="$t('page.form.paid')" :value="0" />
|
||||
<el-option :label="$t('page.form.free')" :value="1" />
|
||||
@@ -60,7 +60,7 @@
|
||||
:placeholder="$t('form.placeholderSelect')"
|
||||
clearable
|
||||
style="width: 100%"
|
||||
:disabled="dialogType === 'edit'"
|
||||
disabled
|
||||
>
|
||||
<el-option :label="$t('page.form.noBigWin')" :value="0" />
|
||||
<el-option :label="$t('page.form.bigWin')" :value="1" />
|
||||
@@ -72,7 +72,7 @@
|
||||
:placeholder="$t('page.form.placeholderWinCoin')"
|
||||
:precision="0"
|
||||
style="width: 100%"
|
||||
:disabled="dialogType === 'edit'"
|
||||
disabled
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('page.form.superWinCoin')" prop="super_win_coin">
|
||||
@@ -82,7 +82,7 @@
|
||||
:precision="0"
|
||||
:min="0"
|
||||
style="width: 100%"
|
||||
:disabled="dialogType === 'edit'"
|
||||
disabled
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('page.form.rewardWinCoin')" prop="reward_win_coin">
|
||||
@@ -92,7 +92,7 @@
|
||||
:precision="0"
|
||||
:min="0"
|
||||
style="width: 100%"
|
||||
:disabled="dialogType === 'edit'"
|
||||
disabled
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('page.form.direction')" prop="direction">
|
||||
@@ -101,7 +101,7 @@
|
||||
:placeholder="$t('page.form.placeholderDirection')"
|
||||
clearable
|
||||
style="width: 100%"
|
||||
:disabled="dialogType === 'edit'"
|
||||
disabled
|
||||
>
|
||||
<el-option :label="$t('page.form.clockwise')" :value="0" />
|
||||
<el-option :label="$t('page.form.anticlockwise')" :value="1" />
|
||||
@@ -113,7 +113,7 @@
|
||||
:placeholder="$t('page.form.placeholderStartIndex')"
|
||||
:min="0"
|
||||
style="width: 100%"
|
||||
:disabled="dialogType === 'edit'"
|
||||
disabled
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('page.form.targetIndex')" prop="target_index">
|
||||
@@ -122,7 +122,7 @@
|
||||
:placeholder="$t('page.form.placeholderTargetIndex')"
|
||||
:min="0"
|
||||
style="width: 100%"
|
||||
:disabled="dialogType === 'edit'"
|
||||
disabled
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('page.form.rollArray')" prop="rollArrayItems">
|
||||
@@ -137,7 +137,7 @@
|
||||
controls-position="right"
|
||||
placeholder=""
|
||||
class="roll-array-input"
|
||||
:disabled="dialogType === 'edit'"
|
||||
disabled
|
||||
/>
|
||||
</div>
|
||||
<div class="roll-array-hint">{{ $t('page.form.rollArrayHint') }}</div>
|
||||
@@ -150,7 +150,7 @@
|
||||
:max="30"
|
||||
:precision="0"
|
||||
style="width: 100%"
|
||||
:disabled="dialogType === 'edit'"
|
||||
disabled
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('page.form.rewardTier')" prop="reward_tier">
|
||||
@@ -160,7 +160,7 @@
|
||||
clearable
|
||||
filterable
|
||||
style="width: 100%"
|
||||
:disabled="true"
|
||||
disabled
|
||||
>
|
||||
<el-option label="T1" value="T1" />
|
||||
<el-option label="T2" value="T2" />
|
||||
@@ -172,19 +172,19 @@
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="handleClose">{{ dialogType === 'edit' ? $t('form.close') : $t('common.cancel') }}</el-button>
|
||||
<el-button v-if="dialogType === 'add'" type="primary" @click="handleSubmit">{{ $t('table.form.submit') }}</el-button>
|
||||
<el-button @click="handleClose">{{ $t('form.close') }}</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import api from '../../../api/play_record/index'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
|
||||
const { t } = useI18n()
|
||||
import { getChannelDeptRequestParams } from '@/composables/useChannelDeptScope'
|
||||
import {
|
||||
lotteryPoolOptionLabel,
|
||||
type LotteryPoolOption
|
||||
} from '@/views/plugin/dice/utils/lotteryPoolDisplay'
|
||||
import type { FormInstance } from 'element-plus'
|
||||
|
||||
interface Props {
|
||||
modelValue: boolean
|
||||
@@ -199,7 +199,7 @@
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
modelValue: false,
|
||||
dialogType: 'add',
|
||||
dialogType: 'edit',
|
||||
data: undefined
|
||||
})
|
||||
|
||||
@@ -212,34 +212,8 @@
|
||||
set: (value) => emit('update:modelValue', value)
|
||||
})
|
||||
|
||||
const rules = reactive<FormRules>({
|
||||
player_id: [{ required: true, message: '请选择玩家', trigger: 'change' }],
|
||||
lottery_config_id: [{ required: true, message: '请选择彩金池配置', trigger: 'change' }],
|
||||
lottery_type: [{ required: true, message: '请选择抽奖类型', trigger: 'change' }],
|
||||
is_win: [{ required: true, message: '请选择是否中大奖', trigger: 'change' }],
|
||||
win_coin: [{ required: true, message: '赢取平台币必填', trigger: 'blur' }],
|
||||
rollArrayItems: [
|
||||
{
|
||||
validator: (_rule: any, value: (number | null)[], callback: (e?: Error) => void) => {
|
||||
if (!value || value.length !== 5) {
|
||||
callback(new Error('摇取点数必须为 5 个数'))
|
||||
return
|
||||
}
|
||||
const ok = value.every((n) => n != null && n >= 1 && n <= 6)
|
||||
if (!ok) {
|
||||
callback(new Error('摇取点数必须填写 5 个数,每个 1~6'))
|
||||
return
|
||||
}
|
||||
callback()
|
||||
},
|
||||
trigger: 'change'
|
||||
}
|
||||
],
|
||||
reward_tier: [{ required: true, message: '请选择中奖档位', trigger: 'change' }]
|
||||
})
|
||||
|
||||
const playerOptions = ref<Array<{ id: number; username: string }>>([])
|
||||
const lotteryConfigOptions = ref<Array<{ id: number; name: string }>>([])
|
||||
const lotteryConfigOptions = ref<LotteryPoolOption[]>([])
|
||||
|
||||
const initialFormData = {
|
||||
id: null as number | null,
|
||||
@@ -272,9 +246,10 @@
|
||||
if (open) {
|
||||
initPage()
|
||||
try {
|
||||
const deptParams = getChannelDeptRequestParams()
|
||||
const [players, lotteryConfigs] = await Promise.all([
|
||||
api.getPlayerOptions(),
|
||||
api.getLotteryConfigOptions()
|
||||
api.getPlayerOptions(deptParams),
|
||||
api.getLotteryConfigOptions(deptParams)
|
||||
])
|
||||
playerOptions.value = Array.isArray(players) ? players : ((players as any)?.data ?? [])
|
||||
lotteryConfigOptions.value = Array.isArray(lotteryConfigs)
|
||||
@@ -374,46 +349,6 @@
|
||||
visible.value = false
|
||||
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(payload)
|
||||
ElMessage.success(t('page.form.addSuccess'))
|
||||
} else {
|
||||
await api.update(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>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -105,6 +105,19 @@
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</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>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -155,10 +155,12 @@
|
||||
import TableSearch from './modules/table-search.vue'
|
||||
import EditDialog from './modules/edit-dialog.vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { lotteryPoolRowLabel } from '@/views/plugin/dice/utils/lotteryPoolDisplay'
|
||||
const { t } = useI18n()
|
||||
|
||||
// 搜索表单(与 play_record 对齐:方向、赢取平台币范围、是否中大奖、中奖档位、点数和)
|
||||
const searchForm = ref<Record<string, unknown>>({
|
||||
lottery_config_id: undefined,
|
||||
reward_config_record_id: undefined,
|
||||
lottery_type: undefined,
|
||||
direction: undefined,
|
||||
@@ -180,8 +182,7 @@
|
||||
return res
|
||||
}
|
||||
|
||||
const lotteryConfigNameFormatter = (row: Record<string, any>) =>
|
||||
row?.diceLotteryPoolConfig?.name ?? row?.lottery_config_id ?? '-'
|
||||
const lotteryConfigNameFormatter = (row: Record<string, any>) => lotteryPoolRowLabel(row)
|
||||
const rewardTierFormatter = (row: Record<string, any>) => row?.reward_tier ?? '-'
|
||||
|
||||
/** 摇取点数格式化为 1,3,4,5,6 */
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="visible"
|
||||
:title="dialogType === 'add' ? $t('page.form.titleAdd') : $t('page.form.titleEdit')"
|
||||
:title="$t('page.form.titleEdit')"
|
||||
width="600px"
|
||||
align-center
|
||||
:close-on-click-modal="false"
|
||||
@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-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 :label="$t('page.form.drawType')" prop="lottery_type">
|
||||
<el-select
|
||||
@@ -17,13 +17,20 @@
|
||||
:placeholder="$t('form.placeholderSelect')"
|
||||
clearable
|
||||
style="width: 100%"
|
||||
disabled
|
||||
>
|
||||
<el-option :label="$t('page.search.paid')" :value="0" />
|
||||
<el-option :label="$t('page.search.free')" :value="1" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<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.anticlockwise')" :value="1" />
|
||||
</el-select>
|
||||
@@ -35,6 +42,7 @@
|
||||
:precision="0"
|
||||
controls-position="right"
|
||||
style="width: 100%"
|
||||
disabled
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('page.search.paidAmount')" prop="paid_amount">
|
||||
@@ -44,10 +52,11 @@
|
||||
:precision="0"
|
||||
controls-position="right"
|
||||
style="width: 100%"
|
||||
disabled
|
||||
/>
|
||||
</el-form-item>
|
||||
<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.bigWin')" :value="1" />
|
||||
</el-select>
|
||||
@@ -59,6 +68,7 @@
|
||||
:precision="0"
|
||||
controls-position="right"
|
||||
style="width: 100%"
|
||||
disabled
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('page.search.rewardTier')" prop="reward_tier">
|
||||
@@ -67,6 +77,7 @@
|
||||
:placeholder="$t('page.form.placeholderRewardTier')"
|
||||
clearable
|
||||
style="width: 100%"
|
||||
disabled
|
||||
>
|
||||
<el-option label="T1" value="T1" />
|
||||
<el-option label="T2" value="T2" />
|
||||
@@ -77,19 +88,19 @@
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<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 :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 :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 :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 :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 :label="$t('page.table.superWinCoin')" prop="super_win_coin">
|
||||
<el-input-number
|
||||
@@ -98,6 +109,7 @@
|
||||
:precision="0"
|
||||
controls-position="right"
|
||||
style="width: 100%"
|
||||
disabled
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('page.table.rewardWinCoin')" prop="reward_win_coin">
|
||||
@@ -107,24 +119,21 @@
|
||||
:precision="0"
|
||||
controls-position="right"
|
||||
style="width: 100%"
|
||||
disabled
|
||||
/>
|
||||
</el-form-item>
|
||||
<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>
|
||||
<template #footer>
|
||||
<el-button @click="handleClose">{{ $t('common.cancel') }}</el-button>
|
||||
<el-button type="primary" @click="handleSubmit">{{ $t('table.form.submit') }}</el-button>
|
||||
<el-button @click="handleClose">{{ $t('form.close') }}</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import api from '../../../api/play_record_test/index'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import type { FormInstance } from 'element-plus'
|
||||
|
||||
interface Props {
|
||||
modelValue: boolean
|
||||
@@ -139,12 +148,11 @@
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
modelValue: false,
|
||||
dialogType: 'add',
|
||||
dialogType: 'edit',
|
||||
data: undefined
|
||||
})
|
||||
|
||||
const emit = defineEmits<Emits>()
|
||||
const { t } = useI18n()
|
||||
|
||||
const formRef = ref<FormInstance>()
|
||||
|
||||
@@ -156,18 +164,6 @@
|
||||
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' }]
|
||||
}))
|
||||
|
||||
/**
|
||||
* 初始数据
|
||||
*/
|
||||
@@ -197,7 +193,7 @@
|
||||
const formData = reactive({ ...initialFormData })
|
||||
|
||||
/**
|
||||
* 监听弹窗打开,初始化表单数据
|
||||
* 监听弹窗打开,初始化表单数据(仅查看)
|
||||
*/
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
@@ -212,9 +208,7 @@
|
||||
* 初始化页面数据
|
||||
*/
|
||||
const initPage = async () => {
|
||||
// 先重置为初始值
|
||||
Object.assign(formData, initialFormData)
|
||||
// 如果有数据,则填充数据
|
||||
if (props.data) {
|
||||
await nextTick()
|
||||
initForm()
|
||||
@@ -222,7 +216,7 @@
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化表单数据
|
||||
* 回填表单数据
|
||||
*/
|
||||
function normalizePlatformCoin(val: unknown): number {
|
||||
if (val === '' || val === null || val === undefined) return 0
|
||||
@@ -244,34 +238,11 @@
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 关闭弹窗并重置表单
|
||||
* 关闭弹窗
|
||||
*/
|
||||
const handleClose = () => {
|
||||
visible.value = false
|
||||
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(payload)
|
||||
ElMessage.success(t('page.form.addSuccess'))
|
||||
} else {
|
||||
await api.update(payload)
|
||||
ElMessage.success(t('page.form.editSuccess'))
|
||||
}
|
||||
emit('success')
|
||||
handleClose()
|
||||
} catch (error) {
|
||||
console.log('表单验证失败:', error)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -8,6 +8,29 @@
|
||||
@search="handleSearch"
|
||||
@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-form-item :label="$t('page.search.rewardConfigRecordId')" prop="reward_config_record_id">
|
||||
<el-input-number
|
||||
@@ -120,6 +143,17 @@
|
||||
</template>
|
||||
|
||||
<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 {
|
||||
modelValue: Record<string, any>
|
||||
}
|
||||
@@ -131,6 +165,57 @@
|
||||
const props = defineProps<Props>()
|
||||
const emit = defineEmits<Emits>()
|
||||
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 formData = computed({
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
<template>
|
||||
<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">
|
||||
<!-- 表格操作 -->
|
||||
@@ -108,15 +109,27 @@
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { useClipboard } from '@vueuse/core'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { useTable } from '@/hooks/core/useTable'
|
||||
import { useSaiAdmin } from '@/composables/useSaiAdmin'
|
||||
import api from '../../api/player/index'
|
||||
import { withChannelDeptParams } from '@/composables/useChannelDeptScope'
|
||||
import TableSearch from './modules/table-search.vue'
|
||||
import EditDialog from './modules/edit-dialog.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 { copy } = useClipboard()
|
||||
const route = useRoute()
|
||||
const routeInit = getRecordRouteInit(route.query)
|
||||
const activeDatePreset = ref<DatePresetKey | null>(routeInit.activePreset)
|
||||
|
||||
// 搜索表单
|
||||
const searchForm = ref({
|
||||
@@ -125,25 +138,77 @@
|
||||
phone: undefined,
|
||||
status: 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>) => {
|
||||
Object.assign(searchParams, params)
|
||||
applySearchParams(params)
|
||||
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 cellValue = row[prop]
|
||||
return cellValue != null && cellValue !== '' ? `${cellValue}%` : '-'
|
||||
}
|
||||
|
||||
// 根据 lottery_config_id 显示彩金池配置名称
|
||||
const lotteryConfigNameFormatter = (row: any) =>
|
||||
row?.diceLotteryPoolConfig?.name ??
|
||||
(row?.lottery_config_id ? `#${row.lottery_config_id}` : t('page.table.customConfig'))
|
||||
const lotteryConfigNameFormatter = (row: any) => {
|
||||
const label = lotteryPoolRowLabel(row)
|
||||
if (label === '-' && !row?.lottery_config_id) {
|
||||
return t('page.table.customConfig')
|
||||
}
|
||||
return label
|
||||
}
|
||||
|
||||
// 表格
|
||||
const {
|
||||
@@ -162,6 +227,8 @@
|
||||
} = useTable({
|
||||
core: {
|
||||
apiFn: api.list,
|
||||
excludeParams: ['create_time'],
|
||||
immediate: !routeInit.hasFilter,
|
||||
columnsFactory: () => [
|
||||
{ type: 'selection' },
|
||||
{ prop: 'username', label: 'page.table.username', align: 'center' },
|
||||
@@ -244,7 +311,7 @@
|
||||
const handleStatusChange = async (row: Record<string, any>, status: number) => {
|
||||
row._statusLoading = true
|
||||
try {
|
||||
await api.updateStatus({ id: row.id, status })
|
||||
await api.updateStatus(withChannelDeptParams({ id: row.id, status }))
|
||||
row.status = status
|
||||
} catch {
|
||||
refreshData()
|
||||
@@ -295,4 +362,10 @@
|
||||
ElMessage.warning(msg)
|
||||
}
|
||||
}
|
||||
|
||||
useRecordRouteSync({
|
||||
searchForm,
|
||||
activeDatePreset,
|
||||
onSearch: handleSearch
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -59,6 +59,7 @@
|
||||
import { ElMessage } from 'element-plus'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { withChannelDeptParams } from '@/composables/useChannelDeptScope'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
@@ -157,12 +158,14 @@
|
||||
return
|
||||
}
|
||||
submitting.value = true
|
||||
await walletRecordApi.adminOperate({
|
||||
await walletRecordApi.adminOperate(
|
||||
withChannelDeptParams({
|
||||
player_id: props.player.id,
|
||||
type: formData.type!,
|
||||
coin,
|
||||
remark: formData.remark?.trim() || undefined
|
||||
})
|
||||
)
|
||||
ElMessage.success(t('page.form.operateSuccess'))
|
||||
emit('success')
|
||||
handleClose()
|
||||
|
||||
@@ -35,7 +35,20 @@
|
||||
<sa-switch v-model="formData.status" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('page.form.adminId')" prop="admin_id">
|
||||
<el-tree-select
|
||||
v-if="useAdminTreeSelect"
|
||||
v-model="formData.admin_id"
|
||||
:data="systemUserTreeOptions"
|
||||
:props="systemUserTreeProps"
|
||||
:placeholder="$t('page.form.placeholderAdminTree')"
|
||||
clearable
|
||||
filterable
|
||||
check-strictly
|
||||
style="width: 100%"
|
||||
:loading="systemUserOptionsLoading"
|
||||
/>
|
||||
<el-select
|
||||
v-else
|
||||
v-model="formData.admin_id"
|
||||
:placeholder="$t('page.form.placeholderAdmin')"
|
||||
clearable
|
||||
@@ -75,7 +88,7 @@
|
||||
<el-option
|
||||
v-for="item in lotteryConfigOptions"
|
||||
:key="item.id"
|
||||
:label="(item.name && String(item.name).trim()) || `#${item.id}`"
|
||||
:label="lotteryPoolOptionLabel(item)"
|
||||
:value="item.id"
|
||||
/>
|
||||
</el-select>
|
||||
@@ -84,12 +97,12 @@
|
||||
<el-form-item v-if="currentLotteryConfig" :label="$t('page.form.currentConfig')" class="current-config-block">
|
||||
<div class="current-lottery-config">
|
||||
<div class="config-row">
|
||||
<span class="config-label">{{ $t('page.form.configLabelName') }}:</span>
|
||||
<span>{{ currentLotteryConfig.name ?? '-' }}</span>
|
||||
<span class="config-label">{{ $t('page.form.configLabelPoolName') }}:</span>
|
||||
<span>{{ lotteryPoolDisplayLabel(currentLotteryConfig) }}</span>
|
||||
</div>
|
||||
<div class="config-row">
|
||||
<span class="config-label">{{ $t('page.form.configLabelType') }}:</span>
|
||||
<span>{{ lotteryConfigTypeText(currentLotteryConfig.name) }}</span>
|
||||
<span class="config-label">{{ $t('page.form.configLabelCode') }}:</span>
|
||||
<span>{{ currentLotteryConfig.name ?? '-' }}</span>
|
||||
</div>
|
||||
<div class="config-row">
|
||||
<span class="config-label">{{ $t('page.form.configLabelWeights') }}:</span>
|
||||
@@ -170,8 +183,18 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
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 {
|
||||
lotteryPoolDisplayLabel,
|
||||
lotteryPoolOptionLabel,
|
||||
type LotteryPoolOption
|
||||
} from '@/views/plugin/dice/utils/lotteryPoolDisplay'
|
||||
import { getChannelDeptRequestParams, withChannelDeptParams } from '@/composables/useChannelDeptScope'
|
||||
import { isSuperAdminUser } from '@/utils/channelLayout'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
|
||||
@@ -222,16 +245,18 @@
|
||||
|
||||
/** 新增时密码必填,编辑时选填 */
|
||||
const passwordRules = computed(() =>
|
||||
props.dialogType === 'add' ? [{ required: true, message: '密码必需填写', trigger: 'blur' }] : []
|
||||
props.dialogType === 'add'
|
||||
? [{ required: true, message: t('page.form.rulePasswordRequired'), trigger: 'blur' }]
|
||||
: []
|
||||
)
|
||||
|
||||
const rules = reactive<FormRules>({
|
||||
username: [{ required: true, message: '用户名必需填写', trigger: 'blur' }],
|
||||
name: [{ required: true, message: '昵称必需填写', trigger: 'blur' }],
|
||||
phone: [{ required: true, message: '手机号必需填写', trigger: 'blur' }],
|
||||
status: [{ required: true, message: '状态必需填写', trigger: 'blur' }],
|
||||
coin: [{ required: true, message: '平台币必需填写', trigger: 'blur' }]
|
||||
})
|
||||
const rules = computed<FormRules>(() => ({
|
||||
username: [{ required: true, message: t('page.form.ruleUsernameRequired'), trigger: 'blur' }],
|
||||
name: [{ required: true, message: t('page.form.ruleNicknameRequired'), trigger: 'blur' }],
|
||||
phone: [{ required: true, message: t('page.form.rulePhoneRequired'), trigger: 'blur' }],
|
||||
status: [{ required: true, message: t('page.form.ruleStatusRequired'), trigger: 'blur' }],
|
||||
coin: [{ required: true, message: t('page.form.ruleCoinRequired'), trigger: 'blur' }]
|
||||
}))
|
||||
|
||||
const initialFormData = {
|
||||
id: null as number | null,
|
||||
@@ -255,23 +280,53 @@
|
||||
const formData = reactive({ ...initialFormData })
|
||||
|
||||
/** 彩金池配置下拉选项(DiceLotteryConfig id、name) */
|
||||
const lotteryConfigOptions = ref<Array<{ id: number; name: string }>>([])
|
||||
const lotteryConfigOptions = ref<LotteryPoolOption[]>([])
|
||||
/** 彩金池选项加载中 */
|
||||
const lotteryConfigLoading = ref(false)
|
||||
/** 后台管理员下拉选项(SystemUser) */
|
||||
const systemUserOptions = ref<
|
||||
Array<{ id: number; username: string; realname: string; label: string }>
|
||||
>([])
|
||||
/** 超管:按渠道分组的管理员树 */
|
||||
const systemUserTreeOptions = ref<
|
||||
Array<{
|
||||
id: number | string
|
||||
label: string
|
||||
disabled?: boolean
|
||||
children?: Array<{ id: number; username: string; realname: string; label: string }>
|
||||
}>
|
||||
>([])
|
||||
const useAdminTreeSelect = computed(() => isSuperAdminUser())
|
||||
const systemUserTreeProps = {
|
||||
label: 'label',
|
||||
value: 'id',
|
||||
children: 'children',
|
||||
disabled: 'disabled'
|
||||
}
|
||||
/** 管理员选项加载中 */
|
||||
const systemUserOptionsLoading = ref(false)
|
||||
/** 当前选中的 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 {
|
||||
const n = String(name ?? '')
|
||||
if (n === 'default') return '默认'
|
||||
if (n === 'killScore') return '杀分'
|
||||
if (n === 'up') return '上分'
|
||||
if (n === 'default') return t('page.form.configTypeDefault')
|
||||
if (n === 'killScore') return t('page.form.configTypeKillScore')
|
||||
if (n === 'up') return t('page.form.configTypeUp')
|
||||
return n || '-'
|
||||
}
|
||||
|
||||
@@ -281,6 +336,15 @@
|
||||
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 */
|
||||
async function loadCurrentLotteryConfig() {
|
||||
const id = formData.lottery_config_id
|
||||
@@ -290,12 +354,11 @@
|
||||
}
|
||||
try {
|
||||
const res = await lotteryConfigApi.read(id)
|
||||
const row = (res as any)?.data ?? (res as any)
|
||||
if (row && typeof row === 'object') {
|
||||
const payload = extractReadPayload(res)
|
||||
if (payload) {
|
||||
const row = parseLotteryPoolConfigOption(payload)
|
||||
currentLotteryConfig.value = row
|
||||
WEIGHT_FIELDS.forEach((key) => {
|
||||
;(formData as any)[key] = Number(row[key] ?? 0)
|
||||
})
|
||||
applyPoolWeightsToForm(row)
|
||||
} else {
|
||||
currentLotteryConfig.value = null
|
||||
}
|
||||
@@ -304,6 +367,15 @@
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => currentLotteryConfig.value,
|
||||
(cfg) => {
|
||||
if (cfg && !isLotteryConfigEmpty()) {
|
||||
applyPoolWeightsToForm(cfg)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(newVal) => {
|
||||
@@ -319,11 +391,10 @@
|
||||
}
|
||||
try {
|
||||
const res = await lotteryConfigApi.read(lotteryConfigId)
|
||||
const row = (res as any)?.data ?? (res as any)
|
||||
if (row && typeof row === 'object') {
|
||||
WEIGHT_FIELDS.forEach((key) => {
|
||||
;(formData as any)[key] = Number(row[key] ?? 0)
|
||||
})
|
||||
const payload = extractReadPayload(res)
|
||||
if (payload) {
|
||||
const row = parseLotteryPoolConfigOption(payload)
|
||||
applyPoolWeightsToForm(row)
|
||||
currentLotteryConfig.value = row
|
||||
} else {
|
||||
currentLotteryConfig.value = null
|
||||
@@ -335,12 +406,43 @@
|
||||
}
|
||||
|
||||
/** 加载后台管理员选项 */
|
||||
function normalizeAdminTreeLabels(
|
||||
nodes: Array<{
|
||||
id: number | string
|
||||
label: string
|
||||
disabled?: boolean
|
||||
children?: Array<{ id: number; username: string; realname: string; label: string }>
|
||||
}>
|
||||
) {
|
||||
return nodes.map((node) => {
|
||||
const item = { ...node }
|
||||
if (item.label === '__unassigned__') {
|
||||
item.label = t('page.form.unassignedChannel')
|
||||
}
|
||||
if (item.children?.length) {
|
||||
item.children = item.children.map((child) => ({
|
||||
...child,
|
||||
label: child.label || child.username || `#${child.id}`
|
||||
}))
|
||||
}
|
||||
return item
|
||||
})
|
||||
}
|
||||
|
||||
async function loadSystemUserOptions() {
|
||||
systemUserOptionsLoading.value = true
|
||||
try {
|
||||
systemUserOptions.value = await api.getSystemUserOptions()
|
||||
if (useAdminTreeSelect.value) {
|
||||
const tree = await api.getSystemUserTreeOptions(getChannelDeptRequestParams())
|
||||
systemUserTreeOptions.value = normalizeAdminTreeLabels(tree)
|
||||
systemUserOptions.value = []
|
||||
} else {
|
||||
systemUserOptions.value = await api.getSystemUserOptions(getChannelDeptRequestParams())
|
||||
systemUserTreeOptions.value = []
|
||||
}
|
||||
} catch {
|
||||
systemUserOptions.value = []
|
||||
systemUserTreeOptions.value = []
|
||||
} finally {
|
||||
systemUserOptionsLoading.value = false
|
||||
}
|
||||
@@ -363,7 +465,7 @@
|
||||
async function loadLotteryConfigOptions() {
|
||||
lotteryConfigLoading.value = true
|
||||
try {
|
||||
lotteryConfigOptions.value = await api.getLotteryConfigOptions()
|
||||
lotteryConfigOptions.value = await api.getLotteryConfigOptions(getChannelDeptRequestParams())
|
||||
} catch {
|
||||
lotteryConfigOptions.value = []
|
||||
} finally {
|
||||
@@ -376,6 +478,7 @@
|
||||
'status',
|
||||
'coin',
|
||||
'lottery_config_id',
|
||||
'admin_id',
|
||||
't1_weight',
|
||||
't2_weight',
|
||||
't3_weight',
|
||||
@@ -397,7 +500,7 @@
|
||||
;(formData as any)[key] = val != null ? Number(val) || null : null
|
||||
} else if (key === 'lottery_config_id' || key === 'admin_id') {
|
||||
const num = Number(val)
|
||||
;(formData as any)[key] = val != null && !Number.isNaN(num) && num !== 0 ? num : null
|
||||
;(formData as any)[key] = val != null && !Number.isNaN(num) && num > 0 ? num : null
|
||||
} else {
|
||||
;(formData as any)[key] = Number(val) || 0
|
||||
}
|
||||
@@ -421,18 +524,21 @@
|
||||
ElMessage.warning(t('page.form.ruleWeightsSumMustBe100'))
|
||||
return
|
||||
}
|
||||
if (!isLotteryConfigEmpty() && currentLotteryConfig.value) {
|
||||
applyPoolWeightsToForm(currentLotteryConfig.value)
|
||||
}
|
||||
const payload = { ...formData }
|
||||
if (isLotteryConfigEmpty()) {
|
||||
;(payload as any).lottery_config_id = null
|
||||
payload.lottery_config_id = null
|
||||
}
|
||||
if (props.dialogType === 'edit' && !payload.password) {
|
||||
delete (payload as any).password
|
||||
}
|
||||
if (props.dialogType === 'add') {
|
||||
await api.save(payload)
|
||||
await api.save(withChannelDeptParams(payload))
|
||||
ElMessage.success(t('page.form.addSuccess'))
|
||||
} else {
|
||||
await api.update(payload)
|
||||
await api.update(withChannelDeptParams(payload))
|
||||
ElMessage.success(t('page.form.editSuccess'))
|
||||
}
|
||||
emit('success')
|
||||
|
||||
@@ -54,17 +54,31 @@
|
||||
<el-option
|
||||
v-for="item in lotteryConfigOptions"
|
||||
:key="item.id"
|
||||
:label="item.name"
|
||||
:label="lotteryPoolOptionLabel(item)"
|
||||
:value="item.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</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>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import api from '../../../api/player/index'
|
||||
import { lotteryPoolOptionLabel, type LotteryPoolOption } from '@/views/plugin/dice/utils/lotteryPoolDisplay'
|
||||
|
||||
interface Props {
|
||||
modelValue: Record<string, any>
|
||||
@@ -77,7 +91,7 @@
|
||||
const props = defineProps<Props>()
|
||||
const emit = defineEmits<Emits>()
|
||||
const isExpanded = ref<boolean>(false)
|
||||
const lotteryConfigOptions = ref<Array<{ id: number; name: string }>>([])
|
||||
const lotteryConfigOptions = ref<LotteryPoolOption[]>([])
|
||||
|
||||
/** 从玩家控制器获取 DiceLotteryPoolConfig id/name 列表,用于 lottery_config_id 筛选 */
|
||||
onMounted(async () => {
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="visible"
|
||||
:title="dialogType === 'add' ? $t('page.form.dialogTitleAdd') : $t('page.form.dialogTitleEdit')"
|
||||
:title="$t('page.form.dialogTitleEdit')"
|
||||
width="600px"
|
||||
align-center
|
||||
:close-on-click-modal="false"
|
||||
@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-select
|
||||
v-model="formData.player_id"
|
||||
@@ -15,7 +15,7 @@
|
||||
clearable
|
||||
filterable
|
||||
style="width: 100%"
|
||||
:disabled="dialogType === 'edit'"
|
||||
disabled
|
||||
>
|
||||
<el-option
|
||||
v-for="item in playerOptions"
|
||||
@@ -30,7 +30,7 @@
|
||||
v-model="formData.use_coins"
|
||||
:placeholder="$t('page.form.placeholderUseCoins')"
|
||||
:min="0"
|
||||
:disabled="dialogType === 'edit'"
|
||||
disabled
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('page.form.paidDrawCount')" prop="paid_ticket_count">
|
||||
@@ -38,8 +38,7 @@
|
||||
v-model="formData.paid_ticket_count"
|
||||
:placeholder="$t('page.form.placeholderPaidDrawCount')"
|
||||
:min="0"
|
||||
@change="onTicketCountChange"
|
||||
:disabled="dialogType === 'edit'"
|
||||
disabled
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('page.form.freeDrawCount')" prop="free_ticket_count">
|
||||
@@ -47,8 +46,7 @@
|
||||
v-model="formData.free_ticket_count"
|
||||
:placeholder="$t('page.form.placeholderFreeDrawCount')"
|
||||
:min="0"
|
||||
@change="onTicketCountChange"
|
||||
:disabled="dialogType === 'edit'"
|
||||
disabled
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('page.form.totalDrawCount')" prop="total_ticket_count">
|
||||
@@ -68,24 +66,20 @@
|
||||
maxlength="500"
|
||||
show-word-limit
|
||||
style="width: 100%"
|
||||
:disabled="dialogType === 'edit'"
|
||||
disabled
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="handleClose">{{ $t('common.cancel') }}</el-button>
|
||||
<el-button type="primary" @click="handleSubmit">{{ $t('table.form.submit') }}</el-button>
|
||||
<el-button @click="handleClose">{{ $t('form.close') }}</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import api from '../../../api/player_ticket_record/index'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
|
||||
const { t } = useI18n()
|
||||
import { getChannelDeptRequestParams } from '@/composables/useChannelDeptScope'
|
||||
import type { FormInstance } from 'element-plus'
|
||||
|
||||
interface Props {
|
||||
modelValue: boolean
|
||||
@@ -100,7 +94,7 @@
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
modelValue: false,
|
||||
dialogType: 'add',
|
||||
dialogType: 'edit',
|
||||
data: undefined
|
||||
})
|
||||
|
||||
@@ -116,17 +110,6 @@
|
||||
set: (value) => emit('update:modelValue', value)
|
||||
})
|
||||
|
||||
/**
|
||||
* 表单验证规则
|
||||
*/
|
||||
const rules = reactive<FormRules>({
|
||||
player_id: [{ required: true, message: '请选择玩家', trigger: 'change' }],
|
||||
use_coins: [{ required: true, message: '消耗硬币必需填写', trigger: 'blur' }],
|
||||
paid_ticket_count: [{ required: true, message: '购买抽奖次数必需填写', trigger: 'blur' }],
|
||||
free_ticket_count: [{ required: true, message: '赠送抽奖次数必需填写', trigger: 'blur' }],
|
||||
remark: [{ required: true, message: '备注必需填写', trigger: 'blur' }]
|
||||
})
|
||||
|
||||
/** 玩家下拉选项(id、username) */
|
||||
const playerOptions = ref<Array<{ id: number; username: string }>>([])
|
||||
|
||||
@@ -137,10 +120,6 @@
|
||||
return paid + free
|
||||
})
|
||||
|
||||
function onTicketCountChange() {
|
||||
formData.total_ticket_count = totalTicketCountComputed.value
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始数据
|
||||
*/
|
||||
@@ -160,7 +139,7 @@
|
||||
const formData = reactive({ ...initialFormData })
|
||||
|
||||
/**
|
||||
* 监听弹窗打开,初始化表单并拉取玩家选项(与 player_wallet_record 一致)
|
||||
* 监听弹窗打开,初始化表单并拉取玩家选项
|
||||
*/
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
@@ -168,7 +147,7 @@
|
||||
if (open) {
|
||||
initPage()
|
||||
try {
|
||||
const list = await api.getPlayerOptions()
|
||||
const list = await api.getPlayerOptions(getChannelDeptRequestParams())
|
||||
const arr = Array.isArray(list) ? list : (list as any)?.data
|
||||
playerOptions.value = Array.isArray(arr)
|
||||
? (arr as Array<{ id: number; username: string }>)
|
||||
@@ -181,7 +160,7 @@
|
||||
)
|
||||
|
||||
/**
|
||||
* 初始化页面数据(仅重置表单、回填编辑数据,不在此处请求玩家列表)
|
||||
* 初始化页面数据
|
||||
*/
|
||||
const initPage = async () => {
|
||||
Object.assign(formData, { ...initialFormData })
|
||||
@@ -192,7 +171,7 @@
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化表单数据
|
||||
* 回填表单数据
|
||||
*/
|
||||
const initForm = () => {
|
||||
if (!props.data) return
|
||||
@@ -214,34 +193,10 @@
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭弹窗并重置表单
|
||||
* 关闭弹窗
|
||||
*/
|
||||
const handleClose = () => {
|
||||
visible.value = false
|
||||
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(rest)
|
||||
ElMessage.success(t('page.form.addSuccess'))
|
||||
} else {
|
||||
await api.update(formData)
|
||||
ElMessage.success(t('page.form.editSuccess'))
|
||||
}
|
||||
emit('success')
|
||||
handleClose()
|
||||
} catch (error) {
|
||||
console.log('表单验证失败:', error)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,12 +1,26 @@
|
||||
<template>
|
||||
<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">
|
||||
<!-- 表格头部 -->
|
||||
<ArtTableHeader v-model:columns="columnChecks" :loading="loading" @refresh="refreshData">
|
||||
<template #left>
|
||||
<span v-if="totalCoinChange !== null" class="table-summary-inline">
|
||||
{{ $t('page.toolbar.coinChangeSummary') }}:<strong :class="coinSummaryClass">{{
|
||||
formatMoney2(totalCoinChange)
|
||||
}}</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>
|
||||
<!-- <ElSpace wrap>-->
|
||||
<!-- <ElButton-->
|
||||
<!-- v-permission="'dice:player_wallet_record:index:save'"-->
|
||||
@@ -82,33 +96,123 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { useTable } from '@/hooks/core/useTable'
|
||||
import { defaultResponseAdapter } from '@/utils/table/tableUtils'
|
||||
import { useSaiAdmin } from '@/composables/useSaiAdmin'
|
||||
import api from '../../api/player_wallet_record/index'
|
||||
import TableSearch from './modules/table-search.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({
|
||||
type: undefined,
|
||||
type: undefined as number | undefined,
|
||||
username: undefined,
|
||||
coin_min: 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 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(() => {
|
||||
if (totalCoinChange.value === null) return ''
|
||||
if (totalCoinChange.value > 0) return 'coin-summary-positive'
|
||||
if (totalCoinChange.value < 0) return 'coin-summary-negative'
|
||||
return ''
|
||||
})
|
||||
|
||||
// 搜索处理:将 create_time 区间转为 create_time_min / create_time_max
|
||||
const handleSearch = (params: Record<string, any>) => {
|
||||
const WALLET_SEARCH_KEYS = [
|
||||
'type',
|
||||
'username',
|
||||
'coin_min',
|
||||
'coin_max',
|
||||
'create_time_min',
|
||||
'create_time_max'
|
||||
] as const
|
||||
|
||||
let summaryRequestSeq = 0
|
||||
|
||||
const listApi = async (params: Record<string, any>) => {
|
||||
const reqId = ++summaryRequestSeq
|
||||
const res = await api.list(params)
|
||||
if (reqId === summaryRequestSeq) {
|
||||
applyCoinSummary(res as Record<string, unknown> | undefined)
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
const applySearchParams = (params: Record<string, any>) => {
|
||||
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>
|
||||
WALLET_SEARCH_KEYS.forEach((key) => {
|
||||
delete paramsRecord[key]
|
||||
})
|
||||
Object.assign(searchParams, p)
|
||||
}
|
||||
|
||||
// 搜索处理:将 create_time 区间转为 create_time_min / create_time_max
|
||||
const handleSearch = (params: Record<string, any>) => {
|
||||
applySearchParams(params)
|
||||
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()
|
||||
// 类型展示:0=充值 1=提现 2=购买抽奖次数 3=管理员加点 4=管理员扣点 5=抽奖
|
||||
const typeFormatter = (row: Record<string, unknown>) => {
|
||||
@@ -169,8 +273,10 @@
|
||||
refreshData
|
||||
} = useTable({
|
||||
core: {
|
||||
apiFn: api.list,
|
||||
apiFn: listApi,
|
||||
apiParams: { limit: 100 },
|
||||
excludeParams: ['create_time'],
|
||||
immediate: !routeInit.hasFilter,
|
||||
columnsFactory: () => [
|
||||
{ type: 'selection', align: 'center' },
|
||||
{ prop: 'id', label: 'page.table.id', width: 80, align: 'center' },
|
||||
@@ -231,6 +337,28 @@
|
||||
useSlot: true
|
||||
}
|
||||
]
|
||||
},
|
||||
hooks: {
|
||||
onSuccess(_data, response) {
|
||||
applyCoinSummary(response as unknown as Record<string, unknown>)
|
||||
}
|
||||
},
|
||||
transform: {
|
||||
responseAdapter(response) {
|
||||
const raw = (response ?? {}) as Record<string, unknown>
|
||||
const base = defaultResponseAdapter(response)
|
||||
const extra = base as Record<string, unknown>
|
||||
if (raw.total_coin_change !== undefined && raw.total_coin_change !== null) {
|
||||
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
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -245,9 +373,35 @@
|
||||
handleSelectionChange
|
||||
// selectedRows
|
||||
} = useSaiAdmin()
|
||||
|
||||
useRecordRouteSync({
|
||||
searchForm,
|
||||
activeDatePreset,
|
||||
onSearch: handleSearch,
|
||||
withType: true
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.table-summary-inline {
|
||||
margin-right: 12px;
|
||||
font-size: 14px;
|
||||
color: var(--el-text-color-regular);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.table-summary-inline strong {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.coin-summary-positive {
|
||||
color: var(--el-color-success);
|
||||
}
|
||||
|
||||
.coin-summary-negative {
|
||||
color: var(--el-color-danger);
|
||||
}
|
||||
|
||||
/* 类型 tag 放大一倍(large + scale) */
|
||||
:deep(.wallet-record-type-tag) {
|
||||
transform: scale(0.8);
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="visible"
|
||||
:title="dialogType === 'add' ? $t('page.form.dialogTitleAdd') : $t('page.form.dialogTitleEdit')"
|
||||
:title="$t('page.form.dialogTitleEdit')"
|
||||
width="600px"
|
||||
align-center
|
||||
:close-on-click-modal="false"
|
||||
@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-select
|
||||
v-model="formData.player_id"
|
||||
@@ -15,8 +15,7 @@
|
||||
clearable
|
||||
filterable
|
||||
style="width: 100%"
|
||||
:disabled="dialogType === 'edit'"
|
||||
@change="onPlayerChange"
|
||||
disabled
|
||||
>
|
||||
<el-option
|
||||
v-for="item in playerOptions"
|
||||
@@ -32,7 +31,7 @@
|
||||
:placeholder="$t('page.form.placeholderType')"
|
||||
clearable
|
||||
style="width: 100%"
|
||||
:disabled="dialogType === 'edit'"
|
||||
disabled
|
||||
>
|
||||
<el-option :label="$t('page.form.typeRecharge')" :value="0" />
|
||||
<el-option :label="$t('page.form.typeWithdraw')" :value="1" />
|
||||
@@ -48,8 +47,7 @@
|
||||
:precision="2"
|
||||
:step="1"
|
||||
style="width: 100%"
|
||||
@change="onCoinChange"
|
||||
:disabled="dialogType === 'edit'"
|
||||
disabled
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('page.form.walletBefore')" prop="wallet_before">
|
||||
@@ -78,24 +76,20 @@
|
||||
:placeholder="$t('page.form.placeholderRemark')"
|
||||
maxlength="500"
|
||||
show-word-limit
|
||||
:disabled="dialogType === 'edit'"
|
||||
disabled
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="handleClose">{{ $t('common.cancel') }}</el-button>
|
||||
<el-button type="primary" @click="handleSubmit">{{ $t('table.form.submit') }}</el-button>
|
||||
<el-button @click="handleClose">{{ $t('form.close') }}</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import api from '../../../api/player_wallet_record/index'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
|
||||
const { t } = useI18n()
|
||||
import { getChannelDeptRequestParams } from '@/composables/useChannelDeptScope'
|
||||
import type { FormInstance } from 'element-plus'
|
||||
|
||||
interface Props {
|
||||
modelValue: boolean
|
||||
@@ -110,7 +104,7 @@
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
modelValue: false,
|
||||
dialogType: 'add',
|
||||
dialogType: 'edit',
|
||||
data: undefined
|
||||
})
|
||||
|
||||
@@ -126,12 +120,6 @@
|
||||
set: (value) => emit('update:modelValue', value)
|
||||
})
|
||||
|
||||
const rules = reactive<FormRules>({
|
||||
player_id: [{ required: true, message: '请选择用户', trigger: 'change' }],
|
||||
coin: [{ required: true, message: '平台币变化必填', trigger: 'blur' }],
|
||||
type: [{ required: true, message: '请选择类型', trigger: 'change' }]
|
||||
})
|
||||
|
||||
const initialFormData: {
|
||||
id: number | null
|
||||
player_id: number | null
|
||||
@@ -152,43 +140,13 @@
|
||||
|
||||
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(
|
||||
() => props.modelValue,
|
||||
async (open) => {
|
||||
if (open) {
|
||||
initPage()
|
||||
try {
|
||||
const list = await api.getPlayerOptions()
|
||||
const list = await api.getPlayerOptions(getChannelDeptRequestParams())
|
||||
playerOptions.value = Array.isArray(list) ? list : []
|
||||
} catch {
|
||||
playerOptions.value = []
|
||||
@@ -229,24 +187,4 @@
|
||||
visible.value = false
|
||||
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(payload)
|
||||
ElMessage.success(t('page.form.addSuccess'))
|
||||
} else {
|
||||
await api.update(payload)
|
||||
ElMessage.success(t('page.form.editSuccess'))
|
||||
}
|
||||
emit('success')
|
||||
handleClose()
|
||||
} catch (error) {
|
||||
console.log('表单验证失败:', error)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -46,17 +46,25 @@
|
||||
</ElCard>
|
||||
|
||||
<WeightRatioDialog v-model="weightRatioVisible" @success="refreshData" />
|
||||
<WeightTestDialog v-model="weightTestVisible" @success="refreshData" />
|
||||
<WeightTestDialog
|
||||
v-model="weightTestVisible"
|
||||
:channel-dept-id="channelDeptId"
|
||||
@success="refreshData"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useTable } from '@/hooks/core/useTable'
|
||||
import { useInjectedChannelDept } from '@/composables/useChannelDeptScope'
|
||||
import api from '../../api/reward/index'
|
||||
import TableSearch from './modules/table-search.vue'
|
||||
import WeightRatioDialog from './modules/weight-ratio-dialog.vue'
|
||||
import WeightTestDialog from './modules/weight-test-dialog.vue'
|
||||
|
||||
const channelScope = useInjectedChannelDept()
|
||||
const channelDeptId = computed(() => channelScope?.selectedDeptId.value)
|
||||
|
||||
const currentDirection = ref<0 | 1>(0)
|
||||
const weightRatioVisible = ref(false)
|
||||
const weightTestVisible = ref(false)
|
||||
@@ -70,13 +78,6 @@
|
||||
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>) => {
|
||||
Object.assign(searchParams, { ...params, direction: currentDirection.value })
|
||||
getData()
|
||||
@@ -124,13 +125,6 @@
|
||||
align: 'center',
|
||||
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: '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">
|
||||
<div v-if="getTierItems(t).length === 0" class="empty-tip">{{ $t('page.weightShared.emptyTier') }}</div>
|
||||
<template v-else>
|
||||
<div class="chart-wrap" v-if="t !== 'T4' && t !== 'T5'">
|
||||
<div class="chart-wrap">
|
||||
<div class="chart-row">
|
||||
<ArtBarChart
|
||||
:x-axis-name="$t('page.weightShared.xAxisEndIndex')"
|
||||
@@ -31,7 +31,7 @@
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="weight-sum" v-if="t !== 'T4' && t !== 'T5'">
|
||||
<div class="weight-sum">
|
||||
{{
|
||||
$t('page.weightShared.sumLineDual', {
|
||||
cw: getTierSum(t, 'clockwise'),
|
||||
@@ -39,7 +39,6 @@
|
||||
})
|
||||
}}
|
||||
</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-column
|
||||
:label="$t('page.weightShared.colEndIndexId')"
|
||||
@@ -54,17 +53,6 @@
|
||||
width="80"
|
||||
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
|
||||
:label="$t('page.weightShared.colUiText')"
|
||||
prop="ui_text"
|
||||
@@ -89,7 +77,6 @@
|
||||
:max="10000"
|
||||
:step="1"
|
||||
size="small"
|
||||
:disabled="isWeightDisabled(row, t)"
|
||||
class="weight-slider"
|
||||
@update:model-value="
|
||||
(v: number | number[]) =>
|
||||
@@ -106,7 +93,7 @@
|
||||
<el-button
|
||||
type="primary"
|
||||
link
|
||||
:disabled="isWeightDisabled(row, t) || getItemWeight(row, 'clockwise') <= 1"
|
||||
:disabled="getItemWeight(row, 'clockwise') <= 1"
|
||||
@click="
|
||||
setItemWeightByRow(
|
||||
t,
|
||||
@@ -122,7 +109,6 @@
|
||||
:min="1"
|
||||
:max="10000"
|
||||
:step="1"
|
||||
:disabled="isWeightDisabled(row, t)"
|
||||
controls-position="right"
|
||||
size="small"
|
||||
class="weight-input"
|
||||
@@ -140,7 +126,7 @@
|
||||
type="primary"
|
||||
link
|
||||
:disabled="
|
||||
isWeightDisabled(row, t) || getItemWeight(row, 'clockwise') >= 10000
|
||||
getItemWeight(row, 'clockwise') >= 10000
|
||||
"
|
||||
@click="
|
||||
setItemWeightByRow(
|
||||
@@ -166,7 +152,6 @@
|
||||
:max="10000"
|
||||
:step="1"
|
||||
size="small"
|
||||
:disabled="isWeightDisabled(row, t)"
|
||||
class="weight-slider"
|
||||
@update:model-value="
|
||||
(v: number | number[]) =>
|
||||
@@ -184,7 +169,7 @@
|
||||
type="primary"
|
||||
link
|
||||
:disabled="
|
||||
isWeightDisabled(row, t) || getItemWeight(row, 'counterclockwise') <= 1
|
||||
getItemWeight(row, 'counterclockwise') <= 1
|
||||
"
|
||||
@click="
|
||||
setItemWeightByRow(
|
||||
@@ -201,7 +186,6 @@
|
||||
:min="1"
|
||||
:max="10000"
|
||||
:step="1"
|
||||
:disabled="isWeightDisabled(row, t)"
|
||||
controls-position="right"
|
||||
size="small"
|
||||
class="weight-input"
|
||||
@@ -219,7 +203,6 @@
|
||||
type="primary"
|
||||
link
|
||||
:disabled="
|
||||
isWeightDisabled(row, t) ||
|
||||
getItemWeight(row, 'counterclockwise') >= 10000
|
||||
"
|
||||
@click="
|
||||
@@ -262,6 +245,7 @@
|
||||
}
|
||||
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { getChannelDeptRequestParams } from '@/composables/useChannelDeptScope'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
@@ -360,11 +344,6 @@
|
||||
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 {
|
||||
const num = typeof v === 'number' && !Number.isNaN(v) ? v : Number(v)
|
||||
if (Number.isNaN(num)) return 1
|
||||
@@ -447,7 +426,7 @@
|
||||
function loadData() {
|
||||
loading.value = true
|
||||
api
|
||||
.weightRatioListWithDirection()
|
||||
.weightRatioListWithDirection(getChannelDeptRequestParams())
|
||||
.then((res: any) => {
|
||||
grouped.value = parsePayload(res)
|
||||
})
|
||||
@@ -464,8 +443,8 @@
|
||||
const items: Array<{ id: number; weight: number }> = []
|
||||
for (const t of TIER_KEYS) {
|
||||
for (const row of getTierItems(t)) {
|
||||
const w0 = isWeightDisabled(row, t) ? 10000 : getItemWeight(row, 'clockwise')
|
||||
const w1 = isWeightDisabled(row, t) ? 10000 : getItemWeight(row, 'counterclockwise')
|
||||
const w0 = getItemWeight(row, 'clockwise')
|
||||
const w1 = getItemWeight(row, 'counterclockwise')
|
||||
const rid0 = row.reward_id_clockwise != null ? row.reward_id_clockwise : 0
|
||||
const rid1 = row.reward_id_counterclockwise != null ? row.reward_id_counterclockwise : 0
|
||||
if (rid0 > 0) items.push({ id: rid0, weight: w0 })
|
||||
@@ -483,7 +462,7 @@
|
||||
}
|
||||
submitting.value = true
|
||||
api
|
||||
.batchUpdateWeights(items)
|
||||
.batchUpdateWeights(items, getChannelDeptRequestParams())
|
||||
.then(() => {
|
||||
ElMessage.success(t('page.weightShared.saveSuccess'))
|
||||
emit('success')
|
||||
@@ -529,9 +508,6 @@
|
||||
margin-bottom: 12px;
|
||||
font-size: 13px;
|
||||
}
|
||||
.weight-sum-t4t5 {
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
.global-tip {
|
||||
margin-bottom: 12px;
|
||||
padding: 10px 12px;
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
<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>
|
||||
<template v-else>
|
||||
<div class="chart-wrap" v-if="t !== 'T4' && t !== 'T5'">
|
||||
<div class="chart-wrap">
|
||||
<ArtBarChart
|
||||
:key="'cw-' + activeDirection + '-' + t"
|
||||
:x-axis-name="$t('page.weightShared.xAxisGridNumber')"
|
||||
@@ -27,12 +27,11 @@
|
||||
height="180px"
|
||||
/>
|
||||
</div>
|
||||
<div class="weight-sum" v-if="t !== 'T4' && t !== 'T5'">
|
||||
<div class="weight-sum">
|
||||
{{
|
||||
$t('page.weightShared.sumLineSingle', { sum: getTierSumForCurrentDirection(t) })
|
||||
}}
|
||||
</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-column
|
||||
:label="$t('page.weightShared.colGridNumber')"
|
||||
@@ -48,17 +47,6 @@
|
||||
align="center"
|
||||
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
|
||||
:label="$t('page.weightShared.colUiText')"
|
||||
prop="ui_text"
|
||||
@@ -87,7 +75,6 @@
|
||||
:max="10000"
|
||||
:step="1"
|
||||
size="small"
|
||||
:disabled="isWeightDisabled(row, t)"
|
||||
class="weight-slider"
|
||||
@update:model-value="
|
||||
(v: number | number[]) =>
|
||||
@@ -103,9 +90,7 @@
|
||||
<el-button
|
||||
type="primary"
|
||||
link
|
||||
:disabled="
|
||||
isWeightDisabled(row, t) || getItemWeightForCurrentDirection(row) <= 1
|
||||
"
|
||||
:disabled="getItemWeightForCurrentDirection(row) <= 1"
|
||||
@click="
|
||||
setItemWeightForCurrentDirection(
|
||||
t,
|
||||
@@ -120,7 +105,6 @@
|
||||
:min="1"
|
||||
:max="10000"
|
||||
:step="1"
|
||||
:disabled="isWeightDisabled(row, t)"
|
||||
controls-position="right"
|
||||
size="small"
|
||||
class="weight-input"
|
||||
@@ -136,10 +120,7 @@
|
||||
<el-button
|
||||
type="primary"
|
||||
link
|
||||
:disabled="
|
||||
isWeightDisabled(row, t) ||
|
||||
getItemWeightForCurrentDirection(row) >= 10000
|
||||
"
|
||||
:disabled="getItemWeightForCurrentDirection(row) >= 10000"
|
||||
@click="
|
||||
setItemWeightForCurrentDirection(
|
||||
t,
|
||||
@@ -163,7 +144,7 @@
|
||||
<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>
|
||||
<template v-else>
|
||||
<div class="chart-wrap" v-if="t !== 'T4' && t !== 'T5'">
|
||||
<div class="chart-wrap">
|
||||
<ArtBarChart
|
||||
:key="'ccw-' + activeDirection + '-' + t"
|
||||
:x-axis-name="$t('page.weightShared.xAxisGridNumber')"
|
||||
@@ -172,12 +153,11 @@
|
||||
height="180px"
|
||||
/>
|
||||
</div>
|
||||
<div class="weight-sum" v-if="t !== 'T4' && t !== 'T5'">
|
||||
<div class="weight-sum">
|
||||
{{
|
||||
$t('page.weightShared.sumLineSingle', { sum: getTierSumForCurrentDirection(t) })
|
||||
}}
|
||||
</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-column
|
||||
:label="$t('page.weightShared.colGridNumber')"
|
||||
@@ -193,13 +173,6 @@
|
||||
align="center"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
<el-table-column
|
||||
:label="$t('page.weightShared.colRealEv')"
|
||||
prop="real_ev"
|
||||
width="90"
|
||||
align="center"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
<el-table-column
|
||||
:label="$t('page.weightShared.colUiText')"
|
||||
prop="ui_text"
|
||||
@@ -228,7 +201,6 @@
|
||||
:max="10000"
|
||||
:step="1"
|
||||
size="small"
|
||||
:disabled="isWeightDisabled(row, t)"
|
||||
class="weight-slider"
|
||||
@update:model-value="
|
||||
(v: number | number[]) =>
|
||||
@@ -244,9 +216,7 @@
|
||||
<el-button
|
||||
type="primary"
|
||||
link
|
||||
:disabled="
|
||||
isWeightDisabled(row, t) || getItemWeightForCurrentDirection(row) <= 1
|
||||
"
|
||||
:disabled="getItemWeightForCurrentDirection(row) <= 1"
|
||||
@click="
|
||||
setItemWeightForCurrentDirection(
|
||||
t,
|
||||
@@ -261,7 +231,6 @@
|
||||
:min="1"
|
||||
:max="10000"
|
||||
:step="1"
|
||||
:disabled="isWeightDisabled(row, t)"
|
||||
controls-position="right"
|
||||
size="small"
|
||||
class="weight-input"
|
||||
@@ -277,10 +246,7 @@
|
||||
<el-button
|
||||
type="primary"
|
||||
link
|
||||
:disabled="
|
||||
isWeightDisabled(row, t) ||
|
||||
getItemWeightForCurrentDirection(row) >= 10000
|
||||
"
|
||||
:disabled="getItemWeightForCurrentDirection(row) >= 10000"
|
||||
@click="
|
||||
setItemWeightForCurrentDirection(
|
||||
t,
|
||||
@@ -319,6 +285,7 @@
|
||||
import ArtBarChart from '@/components/core/charts/art-bar-chart/index.vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { getChannelDeptRequestParams } from '@/composables/useChannelDeptScope'
|
||||
function formatMoney2(val: unknown): string {
|
||||
if (val === '' || val === null || val === undefined) return '-'
|
||||
const n = typeof val === 'number' ? val : Number(val)
|
||||
@@ -432,11 +399,6 @@
|
||||
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 {
|
||||
const num = typeof v === 'number' && !Number.isNaN(v) ? v : Number(v)
|
||||
if (Number.isNaN(num)) return 1
|
||||
@@ -482,7 +444,7 @@
|
||||
function loadData() {
|
||||
loading.value = true
|
||||
api
|
||||
.weightRatioListWithDirection()
|
||||
.weightRatioListWithDirection(getChannelDeptRequestParams())
|
||||
.then((res: any) => {
|
||||
grouped.value = parsePayload(res)
|
||||
})
|
||||
@@ -505,7 +467,7 @@
|
||||
for (const row of list) {
|
||||
const rid = row.reward_id != null ? Number(row.reward_id) : 0
|
||||
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 })
|
||||
}
|
||||
}
|
||||
@@ -521,7 +483,7 @@
|
||||
}
|
||||
submitting.value = true
|
||||
api
|
||||
.batchUpdateWeights(items)
|
||||
.batchUpdateWeights(items, getChannelDeptRequestParams())
|
||||
.then(() => {
|
||||
ElMessage.success(t('page.weightShared.saveSuccess'))
|
||||
emit('success')
|
||||
@@ -566,9 +528,6 @@
|
||||
margin-bottom: 12px;
|
||||
font-size: 13px;
|
||||
}
|
||||
.weight-sum-t4t5 {
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
.global-tip {
|
||||
margin-bottom: 12px;
|
||||
padding: 10px 12px;
|
||||
|
||||
@@ -2,43 +2,85 @@
|
||||
<ElDialog
|
||||
v-model="visible"
|
||||
:title="$t('page.weightTest.title')"
|
||||
width="560px"
|
||||
width="960px"
|
||||
top="4vh"
|
||||
class="weight-test-dialog"
|
||||
:close-on-click-modal="false"
|
||||
destroy-on-close
|
||||
@close="onClose"
|
||||
>
|
||||
<ElAlert type="info" :closable="false" show-icon class="weight-test-tip">
|
||||
<template #title>{{ $t('page.weightTest.alertTitle') }}</template>
|
||||
{{ $t('page.weightTest.alertBody') }}
|
||||
<div class="weight-test-dialog-body">
|
||||
<ElAlert type="info" :closable="false" show-icon class="weight-test-tip compact-tip">
|
||||
<div class="tip-lines">
|
||||
<div>{{ $t('page.weightTest.alertBody') }}</div>
|
||||
<div>{{ $t('page.weightTest.chainModeHint') }}</div>
|
||||
<div>{{ $t('page.weightTest.killModeHint') }}</div>
|
||||
</div>
|
||||
</ElAlert>
|
||||
<ElAlert type="warning" :closable="false" show-icon class="weight-test-tip chain-tip">
|
||||
{{ $t('page.weightTest.chainModeHint') }}
|
||||
</ElAlert>
|
||||
<ElAlert type="info" :closable="false" show-icon class="weight-test-tip chain-tip">
|
||||
{{ $t('page.weightTest.killModeHint') }}
|
||||
</ElAlert>
|
||||
<ElForm :model="form" label-width="140px">
|
||||
<ElFormItem :label="$t('page.weightTest.labelAnte')" prop="ante" required>
|
||||
<ElInputNumber v-model="form.ante" :min="1" :step="1" style="width: 100%" />
|
||||
|
||||
<ElForm :model="form" label-width="108px" class="weight-test-form">
|
||||
<ElRow :gutter="16">
|
||||
<ElCol :span="12">
|
||||
<ElFormItem :label="$t('page.weightTest.labelAnte')" prop="ante_config_id" required>
|
||||
<ElSelect
|
||||
v-model="form.ante_config_id"
|
||||
:placeholder="$t('page.weightTest.placeholderAnte')"
|
||||
filterable
|
||||
style="width: 100%"
|
||||
@change="syncAnteFromSelect"
|
||||
>
|
||||
<ElOption
|
||||
:label="$t('page.weightTest.anteRandomOption')"
|
||||
:value="RANDOM_ANTE_CONFIG_ID"
|
||||
/>
|
||||
<ElOption
|
||||
v-for="item in anteOptions"
|
||||
:key="item.id"
|
||||
:label="anteOptionLabel(item)"
|
||||
:value="item.id"
|
||||
/>
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
<ElFormItem :label="$t('page.weightTest.labelKillModeEnabled')" prop="kill_mode_enabled">
|
||||
<ElSwitch v-model="form.kill_mode_enabled" />
|
||||
</ElFormItem>
|
||||
<ElFormItem :label="$t('page.weightTest.labelTestSafetyLine')" prop="test_safety_line">
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
|
||||
<div class="kill-mode-panel">
|
||||
<div class="kill-mode-head">
|
||||
<div class="kill-mode-title">{{ $t('page.weightTest.killModePanelTitle') }}</div>
|
||||
<ElSwitch
|
||||
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
|
||||
v-model="form.test_safety_line"
|
||||
:min="0"
|
||||
:step="100"
|
||||
:disabled="!form.kill_mode_enabled"
|
||||
style="width: 100%"
|
||||
controls-position="right"
|
||||
class="kill-mode-field-input"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</div>
|
||||
<div class="kill-mode-hint">{{ $t('page.weightTest.testSafetyLineHint') }}</div>
|
||||
<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">
|
||||
<ElCol :span="12">
|
||||
<div class="section-title">{{ $t('page.weightTest.sectionPaid') }}</div>
|
||||
<ElFormItem
|
||||
:label="$t('page.weightTest.labelLotteryTypePaid')"
|
||||
prop="paid_lottery_config_id"
|
||||
>
|
||||
<ElForm :model="form" label-position="top" class="section-form">
|
||||
<ElFormItem :label="$t('page.weightTest.labelLotteryTypePaid')" prop="paid_lottery_config_id">
|
||||
<ElSelect
|
||||
v-model="form.paid_lottery_config_id"
|
||||
:placeholder="$t('page.weightTest.placeholderPaidPool')"
|
||||
@@ -47,16 +89,22 @@
|
||||
style="width: 100%"
|
||||
>
|
||||
<ElOption
|
||||
v-for="item in paidLotteryOptions"
|
||||
:key="item.id"
|
||||
:label="item.name"
|
||||
v-for="item in lotteryOptions"
|
||||
:key="'paid-pool-' + item.id"
|
||||
:label="lotteryPoolOptionLabel(item)"
|
||||
:value="item.id"
|
||||
/>
|
||||
</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>
|
||||
<template v-if="form.paid_lottery_config_id == null">
|
||||
<div class="tier-label">{{ $t('page.weightTest.tierProbHint') }}</div>
|
||||
<ElRow :gutter="12" class="tier-row">
|
||||
<ElRow :gutter="8" class="tier-row">
|
||||
<ElCol v-for="t in tierKeys" :key="'paid-' + t" :span="8">
|
||||
<div class="tier-field">
|
||||
<label class="tier-field-label">{{
|
||||
@@ -78,6 +126,8 @@
|
||||
$t('page.weightTest.tierSumError', { sum: paidTierSum })
|
||||
}}</div>
|
||||
</template>
|
||||
<ElRow :gutter="12">
|
||||
<ElCol :span="12">
|
||||
<ElFormItem :label="$t('page.weightTest.labelCwCount')" prop="paid_s_count" required>
|
||||
<ElSelect
|
||||
v-model="form.paid_s_count"
|
||||
@@ -87,21 +137,26 @@
|
||||
<ElOption v-for="c in countOptions" :key="c" :label="String(c)" :value="c" />
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="12">
|
||||
<ElFormItem :label="$t('page.weightTest.labelCcwCount')" prop="paid_n_count" required>
|
||||
<ElSelect
|
||||
v-model="form.paid_n_count"
|
||||
:placeholder="$t('page.weightTest.placeholderSelect')"
|
||||
style="width: 100%"
|
||||
>
|
||||
<ElOption v-for="c in countOptions" :key="c" :label="String(c)" :value="c" />
|
||||
<ElOption v-for="c in countOptions" :key="'n-' + c" :label="String(c)" :value="c" />
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
</ElForm>
|
||||
</ElCol>
|
||||
|
||||
<ElCol :span="12">
|
||||
<div class="section-title">{{ $t('page.weightTest.sectionFreeAfterPlayAgain') }}</div>
|
||||
<ElFormItem
|
||||
:label="$t('page.weightTest.labelLotteryTypeFree')"
|
||||
prop="free_lottery_config_id"
|
||||
>
|
||||
<ElForm :model="form" label-position="top" class="section-form">
|
||||
<ElFormItem :label="$t('page.weightTest.labelLotteryTypeFree')" prop="free_lottery_config_id">
|
||||
<ElSelect
|
||||
v-model="form.free_lottery_config_id"
|
||||
:placeholder="$t('page.weightTest.placeholderFreePool')"
|
||||
@@ -110,16 +165,22 @@
|
||||
style="width: 100%"
|
||||
>
|
||||
<ElOption
|
||||
v-for="item in freeLotteryOptions"
|
||||
:key="item.id"
|
||||
:label="item.name"
|
||||
v-for="item in lotteryOptions"
|
||||
:key="'free-pool-' + item.id"
|
||||
:label="lotteryPoolOptionLabel(item)"
|
||||
:value="item.id"
|
||||
/>
|
||||
</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>
|
||||
<template v-if="form.free_lottery_config_id == null">
|
||||
<div class="tier-label">{{ $t('page.weightTest.tierProbHintFreeChain') }}</div>
|
||||
<ElRow :gutter="12" class="tier-row">
|
||||
<ElRow :gutter="8" class="tier-row">
|
||||
<ElCol v-for="t in tierKeys" :key="'free-' + t" :span="8">
|
||||
<div class="tier-field">
|
||||
<label class="tier-field-label">{{
|
||||
@@ -142,6 +203,10 @@
|
||||
}}</div>
|
||||
</template>
|
||||
</ElForm>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
</ElForm>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<ElButton
|
||||
@@ -160,11 +225,30 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import api from '../../../api/reward/index'
|
||||
import lotteryPoolApi from '../../../api/lottery_pool_config/index'
|
||||
import anteConfigApi from '../../../api/ante_config/index'
|
||||
import lotteryPoolApi, { type LotteryPoolConfigOption } from '../../../api/lottery_pool_config/index'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import {
|
||||
getChannelDeptRequestParams,
|
||||
useInjectedChannelDept,
|
||||
withChannelDeptParams
|
||||
} from '@/composables/useChannelDeptScope'
|
||||
import {
|
||||
lotteryPoolDisplayLabel,
|
||||
lotteryPoolOptionLabel
|
||||
} from '@/views/plugin/dice/utils/lotteryPoolDisplay'
|
||||
|
||||
/** 底注下拉「随机」选项值(非真实 ante_config.id) */
|
||||
const RANDOM_ANTE_CONFIG_ID = -1
|
||||
|
||||
const props = defineProps<{
|
||||
/** 父页面渠道栏选中值(弹窗 teleport 后 inject 可能失效) */
|
||||
channelDeptId?: number
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
const channelScope = useInjectedChannelDept()
|
||||
|
||||
const countOptions = [0, 100, 500, 1000, 5000]
|
||||
const tierKeys = ['T1', 'T2', 'T3', 'T4', 'T5'] as const
|
||||
@@ -172,8 +256,13 @@
|
||||
const visible = defineModel<boolean>({ default: false })
|
||||
const emit = defineEmits<{ (e: 'success'): void }>()
|
||||
|
||||
const anteOptions = ref<
|
||||
Array<{ id: number; name: string; title: string; mult: number; is_default: number }>
|
||||
>([])
|
||||
|
||||
const form = reactive({
|
||||
ante: 1,
|
||||
ante_config_id: undefined as number | undefined,
|
||||
paid_lottery_config_id: undefined as number | undefined,
|
||||
free_lottery_config_id: undefined as number | undefined,
|
||||
paid_tier_weights: { T1: 20, T2: 20, T3: 20, T4: 20, T5: 20 } as Record<string, number>,
|
||||
@@ -181,18 +270,16 @@
|
||||
paid_s_count: 100,
|
||||
paid_n_count: 100,
|
||||
kill_mode_enabled: false,
|
||||
test_safety_line: 5000
|
||||
test_safety_line: 0
|
||||
})
|
||||
const lotteryOptions = ref<Array<{ id: number; name: string }>>([])
|
||||
/** 付费抽奖券可选档位:name=default */
|
||||
const paidLotteryOptions = computed(() =>
|
||||
lotteryOptions.value.filter((r) => r.name === 'default')
|
||||
const lotteryOptions = ref<LotteryPoolConfigOption[]>([])
|
||||
const selectedPaidPool = computed(() =>
|
||||
lotteryOptions.value.find((r) => r.id === form.paid_lottery_config_id) ?? null
|
||||
)
|
||||
/** 免费抽奖券可选档位:优先 name=killScore,若无则显示全部以便下拉有选项 */
|
||||
const freeLotteryOptions = computed(() => {
|
||||
const list = lotteryOptions.value.filter((r) => r.name === 'killScore')
|
||||
return list.length > 0 ? list : lotteryOptions.value
|
||||
})
|
||||
const selectedFreePool = computed(() =>
|
||||
lotteryOptions.value.find((r) => r.id === form.free_lottery_config_id) ?? null
|
||||
)
|
||||
const defaultPoolInfo = ref<{ safety_line: number; kill_enabled: number; profit_amount: number } | null>(null)
|
||||
const running = ref(false)
|
||||
|
||||
function onClose() {
|
||||
@@ -230,20 +317,96 @@
|
||||
tierKeys.reduce((s, t) => s + (form.free_tier_weights[t] ?? 0), 0)
|
||||
)
|
||||
|
||||
function resolveDeptParams(): { dept_id?: number } {
|
||||
if (props.channelDeptId !== undefined && props.channelDeptId !== null) {
|
||||
return { dept_id: props.channelDeptId }
|
||||
}
|
||||
const extra = getChannelDeptRequestParams()
|
||||
if (extra.dept_id !== undefined) {
|
||||
return extra
|
||||
}
|
||||
if (channelScope) {
|
||||
return { dept_id: channelScope.selectedDeptId.value }
|
||||
}
|
||||
return {}
|
||||
}
|
||||
|
||||
function resolveSubmitDeptId(): number | undefined {
|
||||
const params = resolveDeptParams()
|
||||
if (params.dept_id !== undefined) {
|
||||
return params.dept_id
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
function anteOptionLabel(item: { name: string; title: string; mult: number }): string {
|
||||
const label = (item.title || item.name || '').trim()
|
||||
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() {
|
||||
const opt = anteOptions.value.find((o) => o.id === form.ante_config_id)
|
||||
if (opt) {
|
||||
form.ante = opt.mult
|
||||
}
|
||||
}
|
||||
|
||||
async function loadAnteOptions() {
|
||||
try {
|
||||
const list = await anteConfigApi.getOptions(resolveDeptParams())
|
||||
anteOptions.value = list
|
||||
const def = list.find((i) => i.is_default === 1) ?? list[0]
|
||||
if (def) {
|
||||
form.ante_config_id = def.id
|
||||
form.ante = def.mult
|
||||
} else {
|
||||
form.ante_config_id = undefined
|
||||
form.ante = 1
|
||||
}
|
||||
} catch {
|
||||
anteOptions.value = []
|
||||
form.ante_config_id = undefined
|
||||
}
|
||||
}
|
||||
|
||||
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() {
|
||||
try {
|
||||
const list = await lotteryPoolApi.getOptions()
|
||||
lotteryOptions.value = list.map((r: { id: number; name: string }) => ({
|
||||
id: r.id,
|
||||
name: r.name
|
||||
}))
|
||||
const normal = list.find((r: { name?: string }) => r.name === 'default')
|
||||
if (normal) {
|
||||
const list = await lotteryPoolApi.getOptions(resolveDeptParams())
|
||||
lotteryOptions.value = list
|
||||
const playerDefault = list.find((r) => r.name === 'playerDefault')
|
||||
const normal = list.find((r) => r.name === 'default')
|
||||
if (playerDefault) {
|
||||
form.paid_lottery_config_id = playerDefault.id
|
||||
} else if (normal) {
|
||||
form.paid_lottery_config_id = normal.id
|
||||
}
|
||||
const kill = list.find((r: { name?: string }) => r.name === 'killScore')
|
||||
if (kill) {
|
||||
form.free_lottery_config_id = kill.id
|
||||
const freePool = list.find((r: { name?: string }) => r.name === 'free')
|
||||
if (freePool) {
|
||||
form.free_lottery_config_id = freePool.id
|
||||
} else if (list.length > 0) {
|
||||
form.free_lottery_config_id = list[0].id
|
||||
}
|
||||
@@ -263,6 +426,11 @@
|
||||
kill_mode_enabled: form.kill_mode_enabled,
|
||||
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) {
|
||||
payload.paid_lottery_config_id = form.paid_lottery_config_id
|
||||
} else {
|
||||
@@ -277,7 +445,15 @@
|
||||
}
|
||||
|
||||
function validateForm(): boolean {
|
||||
if (form.ante == null || form.ante <= 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'))
|
||||
return false
|
||||
}
|
||||
if (!isRandomAnte) {
|
||||
syncAnteFromSelect()
|
||||
}
|
||||
if (!isRandomAnte && (form.ante == null || form.ante <= 0)) {
|
||||
ElMessage.warning(t('page.weightTest.warnAnte'))
|
||||
return false
|
||||
}
|
||||
@@ -320,7 +496,12 @@
|
||||
if (!validateForm()) return
|
||||
running.value = true
|
||||
try {
|
||||
await api.startWeightTest(buildPayload())
|
||||
const payload = buildPayload()
|
||||
const deptId = resolveSubmitDeptId()
|
||||
if (deptId !== undefined) {
|
||||
payload.dept_id = deptId
|
||||
}
|
||||
await api.startWeightTest(withChannelDeptParams(payload))
|
||||
ElMessage.success(t('page.weightTest.successCreated'))
|
||||
visible.value = false
|
||||
emit('success')
|
||||
@@ -333,73 +514,212 @@
|
||||
|
||||
watch(visible, (v) => {
|
||||
if (v) {
|
||||
loadLotteryOptions()
|
||||
void loadAnteOptions()
|
||||
void loadLotteryOptions()
|
||||
void loadDefaultPoolInfo()
|
||||
} else {
|
||||
onClose()
|
||||
}
|
||||
})
|
||||
|
||||
watch(
|
||||
() => props.channelDeptId ?? channelScope?.selectedDeptId.value,
|
||||
() => {
|
||||
if (visible.value) {
|
||||
void loadAnteOptions()
|
||||
void loadLotteryOptions()
|
||||
void loadDefaultPoolInfo()
|
||||
}
|
||||
}
|
||||
)
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.weight-test-tip {
|
||||
margin-bottom: 16px;
|
||||
.weight-test-dialog-body {
|
||||
max-height: calc(100vh - 168px);
|
||||
overflow: visible;
|
||||
}
|
||||
.chain-tip {
|
||||
margin-top: -8px;
|
||||
|
||||
.compact-tip {
|
||||
margin-bottom: 12px;
|
||||
:deep(.el-alert__content) {
|
||||
line-height: 1.45;
|
||||
}
|
||||
.section-title {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.tip-lines {
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-regular);
|
||||
div + div {
|
||||
margin-top: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.weight-test-form {
|
||||
:deep(.el-form-item) {
|
||||
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);
|
||||
margin: 8px 0 12px;
|
||||
padding-bottom: 6px;
|
||||
border-bottom: 1px solid var(--el-border-color-lighter);
|
||||
line-height: 1.4;
|
||||
}
|
||||
.tier-label {
|
||||
font-size: 13px;
|
||||
|
||||
.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 {
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--el-text-color-primary);
|
||||
margin: 0 0 10px;
|
||||
padding-bottom: 4px;
|
||||
border-bottom: 1px solid var(--el-border-color-lighter);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.tier-label {
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.tier-row {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.tier-row {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.tier-field {
|
||||
margin-bottom: 12px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.tier-field-label {
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-regular);
|
||||
margin-bottom: 4px;
|
||||
line-height: 1.5;
|
||||
margin-bottom: 2px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.tier-input {
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: 8px 12px;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
padding: 4px 8px;
|
||||
font-size: 13px;
|
||||
line-height: 1.4;
|
||||
color: var(--el-text-color-regular);
|
||||
background-color: var(--el-fill-color-blank);
|
||||
border: 1px solid var(--el-border-color);
|
||||
border-radius: var(--el-border-radius-base);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.tier-input:hover {
|
||||
border-color: var(--el-border-color-hover);
|
||||
}
|
||||
|
||||
.tier-input:focus {
|
||||
outline: none;
|
||||
border-color: var(--el-color-primary);
|
||||
box-shadow: 0 0 0 2px var(--el-color-primary-light-7);
|
||||
}
|
||||
.tier-input::placeholder {
|
||||
color: var(--el-text-color-placeholder);
|
||||
}
|
||||
|
||||
.tier-error {
|
||||
font-size: 12px;
|
||||
color: var(--el-color-danger);
|
||||
margin-top: 4px;
|
||||
margin-bottom: 8px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss">
|
||||
.weight-test-dialog.el-dialog {
|
||||
margin-bottom: 4vh;
|
||||
}
|
||||
.weight-test-dialog .el-dialog__body {
|
||||
padding-top: 12px;
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="art-full-height reward-config-form">
|
||||
<ElCard shadow="never" class="form-card">
|
||||
<div class="reward-config-form flex-1 min-h-0 flex flex-col">
|
||||
<ElCard shadow="never" class="form-card flex-1 min-h-0 flex flex-col">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>{{ $t('page.toolbar.gameRewardConfig') }}</span>
|
||||
@@ -21,9 +21,38 @@
|
||||
<ElTabPane :label="$t('page.configPage.tabIndex')" name="index">
|
||||
<div class="tab-panel">
|
||||
<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
|
||||
v-permission="'dice:reward_config:index:batchUpdate'"
|
||||
v-permission="PERM_TIER_RECOMMEND"
|
||||
type="default"
|
||||
@click="openRuleGenerateDialog"
|
||||
v-ripple
|
||||
@@ -99,38 +128,18 @@
|
||||
<template #default="{ row }">
|
||||
<ElInputNumber
|
||||
v-model="row.real_ev"
|
||||
@change="handleRealEvChange(row)"
|
||||
controls-position="right"
|
||||
size="small"
|
||||
:step="1"
|
||||
:step="0.1"
|
||||
:precision="2"
|
||||
class="full-width"
|
||||
@update:model-value="() => handleRealEvChange(row)"
|
||||
/>
|
||||
</template>
|
||||
</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">
|
||||
<template #default="{ row }">
|
||||
<ElSelect
|
||||
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>
|
||||
<span class="tier-readonly">{{ displayRowTier(row) }}</span>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn
|
||||
@@ -142,7 +151,7 @@
|
||||
<ElInput
|
||||
v-model="row.remark"
|
||||
size="small"
|
||||
:placeholder="$t('page.configPage.placeholderRemark')"
|
||||
:placeholder="remarkPlaceholderForRow(row)"
|
||||
/>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
@@ -167,6 +176,7 @@
|
||||
<ElTable
|
||||
v-loading="loading"
|
||||
:data="bigwinRows"
|
||||
row-key="id"
|
||||
border
|
||||
size="default"
|
||||
class="config-table bigwin-table"
|
||||
@@ -214,11 +224,12 @@
|
||||
<template #default="{ row }">
|
||||
<ElInputNumber
|
||||
v-model="row.real_ev"
|
||||
@change="handleRealEvChange(row)"
|
||||
controls-position="right"
|
||||
size="small"
|
||||
:step="1"
|
||||
:step="0.1"
|
||||
:precision="2"
|
||||
class="full-width"
|
||||
@update:model-value="() => handleRealEvChange(row)"
|
||||
/>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
@@ -231,7 +242,7 @@
|
||||
<ElInput
|
||||
v-model="row.remark"
|
||||
size="small"
|
||||
:placeholder="$t('page.configPage.placeholderRemark')"
|
||||
:placeholder="remarkPlaceholderForRow(row)"
|
||||
/>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
@@ -243,14 +254,17 @@
|
||||
<template #default="{ row }">
|
||||
<div class="weight-cell">
|
||||
<ElSlider
|
||||
v-model="row.weight"
|
||||
:model-value="row.weight"
|
||||
:min="0"
|
||||
:max="10000"
|
||||
:step="100"
|
||||
:disabled="isBigwinWeightDisabled(row)"
|
||||
@update:model-value="
|
||||
(v: number | number[]) => setBigwinRowWeight(row, Array.isArray(v) ? v[0] : v)
|
||||
"
|
||||
/>
|
||||
<ElInputNumber
|
||||
v-model="row.weight"
|
||||
:model-value="row.weight"
|
||||
:min="0"
|
||||
:max="10000"
|
||||
:step="100"
|
||||
@@ -258,6 +272,7 @@
|
||||
controls-position="right"
|
||||
size="small"
|
||||
class="weight-input"
|
||||
@update:model-value="(v: number | undefined) => setBigwinRowWeight(row, v)"
|
||||
/>
|
||||
</div>
|
||||
<span v-if="isBigwinWeightDisabled(row)" class="weight-tip">{{
|
||||
@@ -286,6 +301,7 @@
|
||||
</ElCard>
|
||||
|
||||
<ElDialog
|
||||
v-if="canTierRecommend"
|
||||
v-model="ruleGenerateDialogVisible"
|
||||
:title="$t('page.configPage.ruleGenerateTitle')"
|
||||
:width="ruleGenDialogWidth"
|
||||
@@ -393,6 +409,7 @@
|
||||
<ElInputNumber
|
||||
v-model="ruleGenT4RealEv"
|
||||
class="rule-gen-input-num"
|
||||
:disabled="true"
|
||||
:step="1"
|
||||
:controls="ruleGenInputControls"
|
||||
:size="ruleGenInputSize"
|
||||
@@ -421,8 +438,8 @@
|
||||
<ElInputNumber
|
||||
v-model="ruleGenT5RealEv"
|
||||
class="rule-gen-input-num"
|
||||
:disabled="true"
|
||||
:step="1"
|
||||
:step="0.1"
|
||||
:precision="2"
|
||||
:controls="ruleGenInputControls"
|
||||
:size="ruleGenInputSize"
|
||||
controls-position="right"
|
||||
@@ -442,25 +459,46 @@
|
||||
</div>
|
||||
</template>
|
||||
</ElDialog>
|
||||
|
||||
<CreateRewardReferencePreviewDialog
|
||||
v-model="createRewardPreviewVisible"
|
||||
:dept-id="filterDeptId"
|
||||
@success="loadIndexList"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
DEFAULT_CHANNEL_ID,
|
||||
getChannelDeptRequestParams,
|
||||
useChannelDeptReload,
|
||||
useInjectedChannelDept
|
||||
} from '@/composables/useChannelDeptScope'
|
||||
import { useWindowSize } from '@vueuse/core'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import api from '../../api/reward_config/index'
|
||||
import { checkAuth } from '@/utils/tool'
|
||||
import CreateRewardReferencePreviewDialog from './modules/create-reward-reference-preview-dialog.vue'
|
||||
import {
|
||||
buildRowsFromTiers,
|
||||
computeBoardFrequencies,
|
||||
DEFAULT_TIER_REAL_EV_STANDARDS,
|
||||
defaultRemarkForTier,
|
||||
generateTiers,
|
||||
inferTierFromRealEv,
|
||||
summarizeCounts,
|
||||
type TierRealEvStandards,
|
||||
validateTierRealEvStandards
|
||||
} from '../utils/generateIndexByRules'
|
||||
|
||||
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 isRuleGenMobile = computed(() => viewportWidth.value < 640)
|
||||
@@ -484,37 +522,126 @@
|
||||
weight: number
|
||||
}
|
||||
|
||||
const channelScope = useInjectedChannelDept()
|
||||
const filterDeptId = computed(() => {
|
||||
const scopedId = channelScope?.selectedDeptId.value
|
||||
if (scopedId !== undefined && scopedId !== null) {
|
||||
if (scopedId > 0 || channelScope?.showDefaultTemplate.value) {
|
||||
return scopedId
|
||||
}
|
||||
}
|
||||
const extra = getChannelDeptRequestParams()
|
||||
if (extra.dept_id !== undefined) {
|
||||
return extra.dept_id
|
||||
}
|
||||
return DEFAULT_CHANNEL_ID
|
||||
})
|
||||
|
||||
const activeTab = ref<'index' | 'bigwin'>('index')
|
||||
const loading = ref(false)
|
||||
const savingIndex = ref(false)
|
||||
const savingBigwin = ref(false)
|
||||
const createRewardLoading = ref(false)
|
||||
const createRewardPreviewVisible = ref(false)
|
||||
const ruleGenerateDialogVisible = ref(false)
|
||||
const ruleGenSubmitting = ref(false)
|
||||
const ruleGenT1Fixed = ref(3)
|
||||
const ruleGenT2Min = ref(5)
|
||||
const ruleGenT4Fixed = 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 ruleGenT2RealEv = ref(DEFAULT_TIER_REAL_EV_STANDARDS.T2)
|
||||
const ruleGenT3RealEv = ref(DEFAULT_TIER_REAL_EV_STANDARDS.T3)
|
||||
const ruleGenT4RealEv = ref(DEFAULT_TIER_REAL_EV_STANDARDS.T4)
|
||||
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 */
|
||||
const REWARD_INDEX_MIN = 0
|
||||
const REWARD_INDEX_MAX = 25
|
||||
const ALLOWED_INDEX_TIERS = ['T1', 'T2', 'T3', 'T4', 'T5', 'BIGWIN'] as const
|
||||
|
||||
function isAllowedIndexTier(s: string): boolean {
|
||||
for (let i = 0; i < ALLOWED_INDEX_TIERS.length; i++) {
|
||||
if (ALLOWED_INDEX_TIERS[i] === s) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/** 第一页数据(来自 api.list,即 DiceRewardConfig 表) */
|
||||
const indexRows = ref<IndexRow[]>([])
|
||||
/** 奖励索引 Tab:排除 tier=BIGWIN,仅显示 T1~T5 */
|
||||
@@ -524,6 +651,15 @@
|
||||
/** 原始 list 快照,用于重置 */
|
||||
let indexRowsSnapshot: IndexRow[] = []
|
||||
|
||||
function isAllowedIndexTier(s: string): boolean {
|
||||
for (let i = 0; i < ALLOWED_INDEX_TIERS.length; i++) {
|
||||
if (ALLOWED_INDEX_TIERS[i] === s) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
function toWeight(v: unknown): number {
|
||||
const n = typeof v === 'number' && !Number.isNaN(v) ? v : Number(v)
|
||||
if (Number.isNaN(n)) return 0
|
||||
@@ -543,22 +679,65 @@
|
||||
}
|
||||
}
|
||||
|
||||
function calcRealReward(realEv: unknown): number {
|
||||
const n = typeof realEv === 'number' && !Number.isNaN(realEv) ? realEv : Number(realEv)
|
||||
if (Number.isNaN(n)) {
|
||||
return -1
|
||||
}
|
||||
return n - 1
|
||||
/** BIGWIN 行不参与按 real_ev 推断 T1-T5,避免编辑时从大奖权重表消失 */
|
||||
function setBigwinRowWeight(row: IndexRow, v: number | number[] | undefined | null) {
|
||||
const n = Array.isArray(v) ? v[0] : v
|
||||
row.weight = toWeight(n)
|
||||
}
|
||||
|
||||
function handleRealEvChange(row: IndexRow) {
|
||||
const n =
|
||||
typeof row.real_ev === 'number' && !Number.isNaN(row.real_ev)
|
||||
? row.real_ev
|
||||
: Number(row.real_ev)
|
||||
const text = Number.isNaN(n) ? '' : Number(n).toFixed(2)
|
||||
row.ui_text = text
|
||||
row.ui_text_en = text
|
||||
if (row.tier === 'BIGWIN') {
|
||||
row.remark = defaultRemarkForTier('BIGWIN')
|
||||
return
|
||||
}
|
||||
const n = rowRealEvNumber(row)
|
||||
syncRowTierFromRealEv(row)
|
||||
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 {
|
||||
@@ -569,54 +748,36 @@
|
||||
}
|
||||
|
||||
async function handleCreateRewardReference() {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
t('page.configPage.confirmCreateRefMsg'),
|
||||
t('page.configPage.confirmCreateRefTitle'),
|
||||
{
|
||||
confirmButtonText: t('page.configPage.confirmCreateRefOk'),
|
||||
cancelButtonText: t('page.configPage.confirmCreateRefCancel'),
|
||||
type: 'warning'
|
||||
createRewardPreviewVisible.value = true
|
||||
}
|
||||
)
|
||||
} catch {
|
||||
return
|
||||
|
||||
function extractIndexList(res: unknown): Record<string, unknown>[] {
|
||||
if (Array.isArray(res)) {
|
||||
return res as Record<string, unknown>[]
|
||||
}
|
||||
createRewardLoading.value = true
|
||||
try {
|
||||
const res: any = await api.createRewardReference()
|
||||
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
|
||||
})
|
||||
if (res && typeof res === 'object') {
|
||||
const obj = res as Record<string, unknown>
|
||||
if (Array.isArray(obj.data)) {
|
||||
return obj.data as Record<string, unknown>[]
|
||||
}
|
||||
ElMessage.success(msg)
|
||||
loadIndexList()
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.message ?? t('page.configPage.createRefFail'))
|
||||
} finally {
|
||||
createRewardLoading.value = false
|
||||
if (Array.isArray(obj.records)) {
|
||||
return obj.records as Record<string, unknown>[]
|
||||
}
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
function loadIndexList() {
|
||||
loading.value = true
|
||||
return api
|
||||
.list({ limit: 200 })
|
||||
.then((res: any) => {
|
||||
const list = res?.data?.records ?? res?.records ?? res?.data ?? []
|
||||
const rows = Array.isArray(list)
|
||||
? list.map((r: Record<string, unknown>) => normalizeIndexRow(r))
|
||||
: []
|
||||
.list({ saiType: 'all', limit: 200, dept_id: filterDeptId.value })
|
||||
.then((res: unknown) => {
|
||||
const rows = extractIndexList(res).map((r) => normalizeIndexRow(r))
|
||||
for (const row of rows) {
|
||||
if (row.tier !== 'BIGWIN') {
|
||||
syncRowTierFromRealEv(row)
|
||||
}
|
||||
}
|
||||
indexRows.value = rows
|
||||
indexRowsSnapshot = rows.map((r) => ({ ...r }))
|
||||
})
|
||||
@@ -628,6 +789,17 @@
|
||||
})
|
||||
}
|
||||
|
||||
/** 挂载时拉数;超管切换左侧渠道时重新拉数 */
|
||||
useChannelDeptReload(loadIndexList)
|
||||
watch(
|
||||
() => filterDeptId.value,
|
||||
(deptId, prev) => {
|
||||
if (deptId !== prev) {
|
||||
loadIndexList()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
function isBigwinWeightDisabled(row: IndexRow): boolean {
|
||||
return row.grid_number === 5 || row.grid_number === 30
|
||||
}
|
||||
@@ -700,6 +872,10 @@
|
||||
}
|
||||
|
||||
function openRuleGenerateDialog() {
|
||||
if (!canTierRecommend.value) {
|
||||
return
|
||||
}
|
||||
syncRuleGenFromTierRecommend()
|
||||
ruleGenerateDialogVisible.value = true
|
||||
}
|
||||
|
||||
@@ -736,6 +912,9 @@
|
||||
}
|
||||
|
||||
async function handleRuleGenerateApply() {
|
||||
if (!canTierRecommend.value) {
|
||||
return
|
||||
}
|
||||
const grids = extractGrids26()
|
||||
if (grids === null) {
|
||||
ElMessage.warning(t('page.configPage.ruleGenNeedFullGrid'))
|
||||
@@ -745,12 +924,13 @@
|
||||
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 x5 = Math.max(0, Math.min(26, Math.floor(Number(ruleGenT5Fixed.value))))
|
||||
syncTierRecommendFromRuleGen()
|
||||
const standards = {
|
||||
T1: Number(ruleGenT1RealEv.value),
|
||||
T2: Number(ruleGenT2RealEv.value),
|
||||
T3: Number(ruleGenT3RealEv.value),
|
||||
T4: Number(ruleGenT4RealEv.value),
|
||||
T5: Number(ruleGenT5RealEv.value)
|
||||
T1: Number(tierRecommend.T1),
|
||||
T2: Number(tierRecommend.T2),
|
||||
T3: Number(tierRecommend.T3),
|
||||
T4: Number(tierRecommend.T4),
|
||||
T5: Number(tierRecommend.T5)
|
||||
}
|
||||
const invalidKey = validateTierRealEvStandards(standards)
|
||||
if (invalidKey !== null) {
|
||||
@@ -805,7 +985,7 @@
|
||||
remark: r.remark
|
||||
})
|
||||
)
|
||||
await api.batchUpdate(indexPayload)
|
||||
await api.generateIndexByRules(indexPayload, { dept_id: filterDeptId.value })
|
||||
ElMessage.success(
|
||||
t('page.configPage.ruleGenSuccess', {
|
||||
cwT1: sc.cw.T1,
|
||||
@@ -843,6 +1023,9 @@
|
||||
return
|
||||
}
|
||||
const toSave = indexRows.value.filter((r) => r.tier !== 'BIGWIN')
|
||||
for (const row of toSave) {
|
||||
syncRowTierFromRealEv(row)
|
||||
}
|
||||
savingIndex.value = true
|
||||
try {
|
||||
const indexPayload = toSave.map((r) => ({
|
||||
@@ -854,7 +1037,7 @@
|
||||
tier: r.tier,
|
||||
remark: r.remark
|
||||
}))
|
||||
await api.batchUpdate(indexPayload)
|
||||
await api.batchUpdate(indexPayload, { dept_id: filterDeptId.value })
|
||||
ElMessage.success(t('page.configPage.saveSuccess'))
|
||||
indexRowsSnapshot = indexRows.value.map((r) => ({ ...r }))
|
||||
} catch (e: any) {
|
||||
@@ -912,17 +1095,17 @@
|
||||
ui_text: r.ui_text,
|
||||
ui_text_en: r.ui_text_en,
|
||||
real_ev: r.real_ev,
|
||||
tier: r.tier,
|
||||
tier: 'BIGWIN',
|
||||
remark: r.remark
|
||||
}))
|
||||
await api.batchUpdate(batchPayload)
|
||||
await api.batchUpdate(batchPayload, { dept_id: filterDeptId.value })
|
||||
const weightItems = rows.map((r) => ({
|
||||
grid_number: r.grid_number,
|
||||
weight: isBigwinWeightDisabled(r)
|
||||
? 10000
|
||||
: Math.max(0, Math.min(10000, Math.floor(r.weight)))
|
||||
}))
|
||||
await api.saveBigwinWeightsByGrid(weightItems)
|
||||
await api.saveBigwinWeightsByGrid(weightItems, { dept_id: filterDeptId.value })
|
||||
ElMessage.success(t('page.configPage.saveSuccess'))
|
||||
loadIndexList()
|
||||
} catch (e: any) {
|
||||
@@ -938,9 +1121,6 @@
|
||||
ElMessage.info(t('page.configPage.resetBigwinReloaded'))
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadIndexList()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@@ -1016,6 +1196,61 @@
|
||||
margin-bottom: 12px;
|
||||
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 {
|
||||
display: flex;
|
||||
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>
|
||||
|
||||
@@ -83,6 +83,7 @@
|
||||
import { ElMessage } from 'element-plus'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { withChannelDeptParams } from '@/composables/useChannelDeptScope'
|
||||
|
||||
interface Props {
|
||||
modelValue: boolean
|
||||
@@ -229,10 +230,10 @@
|
||||
delete payload.weight
|
||||
}
|
||||
if (props.dialogType === 'add') {
|
||||
await api.save(payload)
|
||||
await api.save(withChannelDeptParams(payload))
|
||||
ElMessage.success(t('page.form.addSuccess'))
|
||||
} else {
|
||||
await api.update(payload)
|
||||
await api.update(withChannelDeptParams(payload))
|
||||
ElMessage.success(t('page.form.editSuccess'))
|
||||
}
|
||||
emit('success')
|
||||
|
||||
@@ -50,13 +50,6 @@
|
||||
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.colRealEv')"
|
||||
prop="real_ev"
|
||||
width="90"
|
||||
align="center"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
<el-table-column
|
||||
:label="$t('page.weightRatio.colUiText')"
|
||||
prop="ui_text"
|
||||
@@ -173,6 +166,7 @@
|
||||
import ArtBarChart from '@/components/core/charts/art-bar-chart/index.vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { getChannelDeptRequestParams } from '@/composables/useChannelDeptScope'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
@@ -360,7 +354,7 @@
|
||||
|
||||
function loadData() {
|
||||
api
|
||||
.weightRatioList()
|
||||
.weightRatioList(getChannelDeptRequestParams())
|
||||
.then((res: any) => {
|
||||
grouped.value = parseWeightRatioPayload(res)
|
||||
})
|
||||
@@ -393,7 +387,7 @@
|
||||
}
|
||||
submitting.value = true
|
||||
api
|
||||
.batchUpdateWeights(items)
|
||||
.batchUpdateWeights(items, getChannelDeptRequestParams())
|
||||
.then(() => {
|
||||
ElMessage.success(t('page.weightRatio.saveSuccess'))
|
||||
emit('success')
|
||||
|
||||
@@ -53,7 +53,7 @@ export interface TierRealEvStandards {
|
||||
T5: number
|
||||
}
|
||||
|
||||
/** 默认标准(与规则弹窗说明一致) */
|
||||
/** 默认推荐结算金额(满足档位区间规则,可在页面修改) */
|
||||
export const DEFAULT_TIER_REAL_EV_STANDARDS: TierRealEvStandards = {
|
||||
T1: 3,
|
||||
T2: 1.5,
|
||||
@@ -62,6 +62,55 @@ export const DEFAULT_TIER_REAL_EV_STANDARDS: TierRealEvStandards = {
|
||||
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. 前缀)
|
||||
*/
|
||||
@@ -69,10 +118,10 @@ export function validateTierRealEvStandards(s: TierRealEvStandards): string | nu
|
||||
if (!Number.isFinite(s.T1) || !(s.T1 > 2)) {
|
||||
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'
|
||||
}
|
||||
if (!Number.isFinite(s.T3) || !(s.T3 > 0 && s.T3 < 1)) {
|
||||
if (!Number.isFinite(s.T3) || !(s.T3 > 0 && s.T3 <= 1)) {
|
||||
return 'ruleGenInvalidT3RealEv'
|
||||
}
|
||||
if (!Number.isFinite(s.T4) || !(s.T4 < 0)) {
|
||||
@@ -353,7 +402,7 @@ export function buildRowsFromTiers(
|
||||
const f = uiTextByTierWhenStandards(tier, real_ev)
|
||||
ui_text = f.ui_text
|
||||
ui_text_en = f.ui_text_en
|
||||
remark = '前端需要在播放一次动画(特殊)'
|
||||
remark = '再来一次'
|
||||
}
|
||||
} else if (tier === 'T1') {
|
||||
real_ev = 101 + ((id * 17 + grid_number * 3) % 398)
|
||||
@@ -393,10 +442,9 @@ export function buildRowsFromTiers(
|
||||
remark = '惩罚'
|
||||
} else {
|
||||
real_ev = 0
|
||||
const f = uiTextFromRealEv(real_ev)
|
||||
ui_text = f.ui_text
|
||||
ui_text_en = f.ui_text_en
|
||||
remark = '前端需要在播放一次动画(特殊)'
|
||||
ui_text = '再来一次'
|
||||
ui_text_en = 'Once again'
|
||||
remark = '再来一次'
|
||||
}
|
||||
rows.push({
|
||||
id,
|
||||
|
||||
@@ -143,6 +143,35 @@
|
||||
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 {
|
||||
const dash = t('page.detail.dash')
|
||||
@@ -228,8 +257,16 @@
|
||||
{
|
||||
prop: 'ante',
|
||||
label: 'page.table.ante',
|
||||
width: 90,
|
||||
align: 'center'
|
||||
width: 100,
|
||||
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',
|
||||
|
||||
@@ -20,6 +20,12 @@
|
||||
<el-descriptions-item :label="$t('page.detail.paidPlannedSpins')">
|
||||
{{ record.paid_planned_spins ?? $t('page.detail.dash') }}
|
||||
</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')">
|
||||
{{ formatTestCountDisplay(record) }}
|
||||
</el-descriptions-item>
|
||||
@@ -30,10 +36,10 @@
|
||||
{{ record.admin_name ?? record.admin_id ?? $t('page.detail.dash') }}
|
||||
</el-descriptions-item>
|
||||
<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 :label="$t('page.detail.freePoolId')">
|
||||
{{ record.free_lottery_config_id ?? $t('page.detail.dash') }}
|
||||
{{ formatRecordFreePoolName(record) }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item :label="$t('page.detail.bigwinSnapshot')">
|
||||
<template v-if="bigwinWeightDisplay.length">
|
||||
@@ -182,7 +188,7 @@
|
||||
<el-option
|
||||
v-for="opt in paidLotteryOptions"
|
||||
:key="opt.id"
|
||||
:label="opt.name"
|
||||
:label="lotteryPoolOptionLabel(opt)"
|
||||
:value="opt.id"
|
||||
/>
|
||||
</el-select>
|
||||
@@ -199,7 +205,7 @@
|
||||
<el-option
|
||||
v-for="opt in freeLotteryOptions"
|
||||
:key="opt.id"
|
||||
:label="opt.name"
|
||||
:label="lotteryPoolOptionLabel(opt)"
|
||||
:value="opt.id"
|
||||
/>
|
||||
</el-select>
|
||||
@@ -224,6 +230,11 @@
|
||||
import ArtBarChart from '@/components/core/charts/art-bar-chart/index.vue'
|
||||
import recordApi from '../../../api/reward_config_record/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 { useI18n } from 'vue-i18n'
|
||||
|
||||
@@ -242,6 +253,9 @@
|
||||
over_play_count?: number
|
||||
chain_free_mode?: number | boolean | string
|
||||
paid_planned_spins?: number
|
||||
ante?: number
|
||||
kill_mode_enabled?: number
|
||||
test_safety_line?: number
|
||||
create_time?: string
|
||||
admin_id?: number | null
|
||||
admin_name?: string
|
||||
@@ -278,6 +292,35 @@
|
||||
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 {
|
||||
if (!record) return t('page.detail.dash')
|
||||
const status = Number(record.status)
|
||||
@@ -316,7 +359,18 @@
|
||||
const importing = ref(false)
|
||||
const importPaidLotteryConfigId = 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) {
|
||||
const dash = t('page.detail.dash')
|
||||
@@ -496,6 +550,15 @@
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(open) => {
|
||||
if (open) {
|
||||
void loadLotteryOptions()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
function openImport() {
|
||||
importPaidLotteryConfigId.value =
|
||||
props.record?.paid_lottery_config_id ?? props.record?.lottery_config_id ?? null
|
||||
|
||||
@@ -1,35 +1,31 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="visible"
|
||||
:title="dialogType === 'add' ? $t('page.form.titleAdd') : $t('page.form.titleEdit')"
|
||||
:title="$t('page.form.titleEdit')"
|
||||
width="600px"
|
||||
align-center
|
||||
:close-on-click-modal="false"
|
||||
@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-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 :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 :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>
|
||||
<template #footer>
|
||||
<el-button @click="handleClose">{{ $t('common.cancel') }}</el-button>
|
||||
<el-button type="primary" @click="handleSubmit">{{ $t('table.form.submit') }}</el-button>
|
||||
<el-button @click="handleClose">{{ $t('form.close') }}</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import api from '../../../api/reward_config_record/index'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import type { FormInstance } from 'element-plus'
|
||||
|
||||
interface Props {
|
||||
modelValue: boolean
|
||||
@@ -44,12 +40,11 @@
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
modelValue: false,
|
||||
dialogType: 'add',
|
||||
dialogType: 'edit',
|
||||
data: undefined
|
||||
})
|
||||
|
||||
const emit = defineEmits<Emits>()
|
||||
const { t } = useI18n()
|
||||
|
||||
const formRef = ref<FormInstance>()
|
||||
|
||||
@@ -61,13 +56,6 @@
|
||||
set: (value) => emit('update:modelValue', value)
|
||||
})
|
||||
|
||||
/**
|
||||
* 表单验证规则
|
||||
*/
|
||||
const rules = computed<FormRules>(() => ({
|
||||
test_count: [{ required: true, message: t('page.form.ruleTestCountRequired'), trigger: 'blur' }]
|
||||
}))
|
||||
|
||||
/**
|
||||
* 初始数据
|
||||
*/
|
||||
@@ -75,7 +63,7 @@
|
||||
id: null,
|
||||
test_count: 100,
|
||||
weight_config_snapshot: '',
|
||||
result_counts: '',
|
||||
result_counts: ''
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -84,7 +72,7 @@
|
||||
const formData = reactive({ ...initialFormData })
|
||||
|
||||
/**
|
||||
* 监听弹窗打开,初始化表单数据
|
||||
* 监听弹窗打开,初始化表单数据(仅查看)
|
||||
*/
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
@@ -99,9 +87,7 @@
|
||||
* 初始化页面数据
|
||||
*/
|
||||
const initPage = async () => {
|
||||
// 先重置为初始值
|
||||
Object.assign(formData, initialFormData)
|
||||
// 如果有数据,则填充数据
|
||||
if (props.data) {
|
||||
await nextTick()
|
||||
initForm()
|
||||
@@ -109,7 +95,7 @@
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化表单数据
|
||||
* 回填表单数据
|
||||
*/
|
||||
const initForm = () => {
|
||||
if (props.data) {
|
||||
@@ -122,31 +108,10 @@
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭弹窗并重置表单
|
||||
* 关闭弹窗
|
||||
*/
|
||||
const handleClose = () => {
|
||||
visible.value = false
|
||||
formRef.value?.resetFields()
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交表单
|
||||
*/
|
||||
const handleSubmit = async () => {
|
||||
if (!formRef.value) return
|
||||
try {
|
||||
await formRef.value.validate()
|
||||
if (props.dialogType === 'add') {
|
||||
await api.save(formData)
|
||||
ElMessage.success(t('page.form.addSuccess'))
|
||||
} else {
|
||||
await api.update(formData)
|
||||
ElMessage.success(t('page.form.editSuccess'))
|
||||
}
|
||||
emit('success')
|
||||
handleClose()
|
||||
} catch (error) {
|
||||
console.log('表单验证失败:', error)
|
||||
}
|
||||
}
|
||||
</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>
|
||||
@@ -1,10 +1,8 @@
|
||||
<template>
|
||||
<div class="art-full-height">
|
||||
<!-- 搜索面板 -->
|
||||
<TableSearch v-model="searchForm" @search="handleSearch" @reset="resetSearchParams" />
|
||||
|
||||
<ElCard class="art-table-card" shadow="never">
|
||||
<!-- 表格头部 -->
|
||||
<ArtTableHeader v-model:columns="columnChecks" :loading="loading" @refresh="refreshData">
|
||||
<template #left>
|
||||
<ElSpace wrap>
|
||||
@@ -14,30 +12,22 @@
|
||||
</template>
|
||||
{{ $t('table.actions.add') }}
|
||||
</ElButton>
|
||||
<ElButton @click="toggleExpand" v-ripple>
|
||||
<template #icon>
|
||||
<ArtSvgIcon v-if="isExpanded" icon="ri:collapse-diagonal-line" />
|
||||
<ArtSvgIcon v-else icon="ri:expand-diagonal-line" />
|
||||
</template>
|
||||
{{ isExpanded ? $t('table.searchBar.collapse') : $t('table.searchBar.expand') }}
|
||||
<ElButton v-permission="'core:dept:update'" @click="handleSyncConfigs" v-ripple>
|
||||
补齐渠道配置
|
||||
</ElButton>
|
||||
</ElSpace>
|
||||
</template>
|
||||
</ArtTableHeader>
|
||||
|
||||
<!-- 表格 -->
|
||||
<ArtTable
|
||||
ref="tableRef"
|
||||
rowKey="id"
|
||||
:loading="loading"
|
||||
:data="data"
|
||||
:columns="columns"
|
||||
:default-expand-all="true"
|
||||
@sort-change="handleSortChange"
|
||||
@pagination:size-change="handleSizeChange"
|
||||
@pagination:current-change="handleCurrentChange"
|
||||
>
|
||||
<!-- 操作列 -->
|
||||
<template #operation="{ row }">
|
||||
<div class="flex gap-2">
|
||||
<SaButton
|
||||
@@ -48,20 +38,26 @@
|
||||
<SaButton
|
||||
v-permission="'core:dept:destroy'"
|
||||
type="error"
|
||||
@click="deleteRow(row, api.delete, refreshData)"
|
||||
@click="openDeleteDialog(row)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</ArtTable>
|
||||
</ElCard>
|
||||
|
||||
<!-- 编辑弹窗 -->
|
||||
<EditDialog
|
||||
v-model="dialogVisible"
|
||||
:dialog-type="dialogType"
|
||||
:data="dialogData"
|
||||
@success="refreshData"
|
||||
/>
|
||||
|
||||
<DeleteChannelDialog
|
||||
v-model="deleteDialogVisible"
|
||||
:dept-id="deleteDeptId"
|
||||
:dept-name="deleteDeptName"
|
||||
@success="refreshData"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -69,27 +65,26 @@
|
||||
import { useTable } from '@/hooks/core/useTable'
|
||||
import { useSaiAdmin } from '@/composables/useSaiAdmin'
|
||||
import api from '@/api/system/dept'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import TableSearch from './modules/table-search.vue'
|
||||
import EditDialog from './modules/edit-dialog.vue'
|
||||
import DeleteChannelDialog from './modules/delete-channel-dialog.vue'
|
||||
|
||||
// 状态管理
|
||||
const isExpanded = ref(true)
|
||||
const tableRef = ref()
|
||||
|
||||
// 搜索表单
|
||||
const searchForm = ref({
|
||||
name: undefined,
|
||||
code: undefined,
|
||||
status: undefined
|
||||
})
|
||||
|
||||
// 搜索处理
|
||||
const deleteDialogVisible = ref(false)
|
||||
const deleteDeptId = ref<number | null>(null)
|
||||
const deleteDeptName = ref('')
|
||||
|
||||
const handleSearch = (params: Record<string, any>) => {
|
||||
Object.assign(searchParams, params)
|
||||
getData()
|
||||
}
|
||||
|
||||
// 表格配置
|
||||
const {
|
||||
columns,
|
||||
columnChecks,
|
||||
@@ -118,26 +113,20 @@
|
||||
}
|
||||
})
|
||||
|
||||
// 编辑配置
|
||||
const { dialogType, dialogVisible, dialogData, showDialog, deleteRow } = useSaiAdmin()
|
||||
const { dialogType, dialogVisible, dialogData, showDialog } = useSaiAdmin()
|
||||
|
||||
/**
|
||||
* 切换展开/收起所有菜单
|
||||
*/
|
||||
const toggleExpand = (): void => {
|
||||
isExpanded.value = !isExpanded.value
|
||||
nextTick(() => {
|
||||
if (tableRef.value?.elTableRef && data.value) {
|
||||
const processRows = (rows: any[]) => {
|
||||
rows.forEach((row) => {
|
||||
if (row.children?.length) {
|
||||
tableRef.value.elTableRef.toggleRowExpansion(row, isExpanded.value)
|
||||
processRows(row.children)
|
||||
const openDeleteDialog = (row: any) => {
|
||||
deleteDeptId.value = row.id
|
||||
deleteDeptName.value = row.name
|
||||
deleteDialogVisible.value = true
|
||||
}
|
||||
})
|
||||
|
||||
const handleSyncConfigs = async () => {
|
||||
try {
|
||||
await api.syncChannelConfigs()
|
||||
ElMessage.success('已为缺失配置的渠道补齐默认配置')
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.message || '补齐失败')
|
||||
}
|
||||
processRows(data.value)
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="visible"
|
||||
title="删除渠道"
|
||||
width="560px"
|
||||
align-center
|
||||
:close-on-click-modal="false"
|
||||
@close="handleClose"
|
||||
>
|
||||
<div v-loading="loading">
|
||||
<p class="mb-3 text-sm text-gray-600">确定删除渠道「{{ deptName }}」?可勾选一并删除的关联数据:</p>
|
||||
<el-alert
|
||||
v-if="preview?.user_count > 0"
|
||||
type="error"
|
||||
:closable="false"
|
||||
class="mb-3"
|
||||
:title="`该渠道下仍有 ${preview.user_count} 个用户,请先转移或删除用户`"
|
||||
/>
|
||||
<el-checkbox-group v-model="checkedTables" class="flex flex-col gap-2">
|
||||
<el-checkbox
|
||||
v-for="item in preview?.relations || []"
|
||||
:key="item.table"
|
||||
:label="item.table"
|
||||
:disabled="preview?.user_count > 0"
|
||||
>
|
||||
{{ item.label }}({{ item.count }} 条)
|
||||
</el-checkbox>
|
||||
</el-checkbox-group>
|
||||
<p v-if="!preview?.relations?.length" class="text-sm text-gray-500">无关联业务数据,仅删除渠道本身。</p>
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button @click="handleClose">取消</el-button>
|
||||
<el-button
|
||||
type="danger"
|
||||
:loading="submitting"
|
||||
:disabled="preview?.user_count > 0"
|
||||
@click="handleConfirm"
|
||||
>
|
||||
确认删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import api from '@/api/system/dept'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
interface Props {
|
||||
modelValue: boolean
|
||||
deptId?: number | null
|
||||
deptName?: string
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
modelValue: false,
|
||||
deptId: null,
|
||||
deptName: ''
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', value: boolean): void
|
||||
(e: 'success'): void
|
||||
}>()
|
||||
|
||||
const visible = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (v) => emit('update:modelValue', v)
|
||||
})
|
||||
|
||||
const loading = ref(false)
|
||||
const submitting = ref(false)
|
||||
const preview = ref<any>(null)
|
||||
const checkedTables = ref<string[]>([])
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
async (open) => {
|
||||
if (!open || !props.deptId) {
|
||||
return
|
||||
}
|
||||
loading.value = true
|
||||
checkedTables.value = []
|
||||
try {
|
||||
const res: any = await api.destroyPreview(props.deptId)
|
||||
const list = res?.data ?? res
|
||||
preview.value = Array.isArray(list) ? list[0] : list
|
||||
checkedTables.value = (preview.value?.relations || []).map((r: any) => r.table)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
const handleClose = () => {
|
||||
visible.value = false
|
||||
}
|
||||
|
||||
const handleConfirm = async () => {
|
||||
if (!props.deptId) {
|
||||
return
|
||||
}
|
||||
submitting.value = true
|
||||
try {
|
||||
await api.delete({ ids: props.deptId, delete_tables: checkedTables.value })
|
||||
ElMessage.success('删除成功')
|
||||
emit('success')
|
||||
handleClose()
|
||||
} catch (e: unknown) {
|
||||
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 {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -8,15 +8,6 @@
|
||||
@close="handleClose"
|
||||
>
|
||||
<el-form ref="formRef" :model="formData" :rules="rules" label-width="120px">
|
||||
<el-form-item :label="$t('page.form.labelParentDept')" prop="parent_id">
|
||||
<el-tree-select
|
||||
v-model="formData.parent_id"
|
||||
:data="optionData.treeData"
|
||||
:render-after-expand="false"
|
||||
check-strictly
|
||||
clearable
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('page.form.labelDeptName')" prop="name">
|
||||
<el-input v-model="formData.name" :placeholder="$t('page.form.placeholderDeptName')" />
|
||||
</el-form-item>
|
||||
@@ -75,36 +66,21 @@
|
||||
const { t } = useI18n()
|
||||
|
||||
const formRef = ref<FormInstance>()
|
||||
const optionData = reactive({
|
||||
treeData: <any[]>[]
|
||||
})
|
||||
|
||||
/**
|
||||
* 弹窗显示状态双向绑定
|
||||
*/
|
||||
const visible = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (value) => emit('update:modelValue', value)
|
||||
})
|
||||
|
||||
/**
|
||||
* 表单验证规则
|
||||
*/
|
||||
const rules = computed<FormRules>(() => ({
|
||||
parent_id: [
|
||||
{ required: true, message: t('page.form.ruleParentDeptRequired'), trigger: 'change' }
|
||||
],
|
||||
name: [{ required: true, message: t('page.form.ruleDeptNameRequired'), trigger: 'blur' }],
|
||||
code: [{ required: true, message: t('page.form.ruleDeptCodeRequired'), trigger: 'blur' }]
|
||||
}))
|
||||
|
||||
/**
|
||||
* 初始数据
|
||||
*/
|
||||
const initialFormData = {
|
||||
id: null,
|
||||
parent_id: null,
|
||||
level: '',
|
||||
parent_id: 0,
|
||||
level: '0',
|
||||
name: '',
|
||||
code: '',
|
||||
leader_id: null,
|
||||
@@ -113,14 +89,8 @@
|
||||
status: 1
|
||||
}
|
||||
|
||||
/**
|
||||
* 表单数据
|
||||
*/
|
||||
const formData = reactive({ ...initialFormData })
|
||||
|
||||
/**
|
||||
* 监听弹窗打开,初始化表单数据
|
||||
*/
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(newVal) => {
|
||||
@@ -130,33 +100,14 @@
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* 初始化页面数据
|
||||
*/
|
||||
const initPage = async () => {
|
||||
// 先重置为初始值
|
||||
Object.assign(formData, initialFormData)
|
||||
|
||||
const data = await api.list({ tree: true })
|
||||
optionData.treeData = [
|
||||
{
|
||||
id: 0,
|
||||
value: 0,
|
||||
label: t('page.form.noParentDept'),
|
||||
children: data
|
||||
}
|
||||
]
|
||||
|
||||
// 如果有数据,则填充数据
|
||||
if (props.data) {
|
||||
await nextTick()
|
||||
initForm()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化表单数据
|
||||
*/
|
||||
const initForm = () => {
|
||||
if (props.data) {
|
||||
for (const key in formData) {
|
||||
@@ -167,21 +118,17 @@
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭弹窗并重置表单
|
||||
*/
|
||||
const handleClose = () => {
|
||||
visible.value = false
|
||||
formRef.value?.resetFields()
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交表单
|
||||
*/
|
||||
const handleSubmit = async () => {
|
||||
if (!formRef.value) return
|
||||
try {
|
||||
await formRef.value.validate()
|
||||
formData.parent_id = 0
|
||||
formData.level = '0'
|
||||
if (props.dialogType === 'add') {
|
||||
await api.save(formData)
|
||||
ElMessage.success(t('page.form.addSuccess'))
|
||||
|
||||
@@ -1,142 +0,0 @@
|
||||
<template>
|
||||
<div class="art-full-height">
|
||||
<!-- 搜索面板 -->
|
||||
<TableSearch v-model="searchForm" @search="handleSearch" @reset="resetSearchParams" />
|
||||
|
||||
<ElCard class="art-table-card" shadow="never">
|
||||
<!-- 表格头部 -->
|
||||
<ArtTableHeader v-model:columns="columnChecks" :loading="loading" @refresh="refreshData">
|
||||
<template #left>
|
||||
<ElSpace wrap>
|
||||
<ElButton v-permission="'core:post:save'" @click="showDialog('add')" v-ripple>
|
||||
<template #icon>
|
||||
<ArtSvgIcon icon="ri:add-fill" />
|
||||
</template>
|
||||
{{ $t('table.actions.add') }}
|
||||
</ElButton>
|
||||
<ElButton
|
||||
v-permission="'core:post:destroy'"
|
||||
:disabled="selectedRows.length === 0"
|
||||
@click="deleteSelectedRows(api.delete, refreshData)"
|
||||
v-ripple
|
||||
>
|
||||
<template #icon>
|
||||
<ArtSvgIcon icon="ri:delete-bin-5-line" />
|
||||
</template>
|
||||
{{ $t('table.actions.delete') }}
|
||||
</ElButton>
|
||||
<SaImport
|
||||
v-permission="'core:post:import'"
|
||||
download-url="/core/post/downloadTemplate"
|
||||
upload-url="/core/post/import"
|
||||
@success="refreshData"
|
||||
/>
|
||||
<SaExport v-permission="'core:post:export'" url="/core/post/export" />
|
||||
</ElSpace>
|
||||
</template>
|
||||
</ArtTableHeader>
|
||||
|
||||
<!-- 表格 -->
|
||||
<ArtTable
|
||||
ref="tableRef"
|
||||
rowKey="id"
|
||||
:loading="loading"
|
||||
:data="data"
|
||||
:columns="columns"
|
||||
:pagination="pagination"
|
||||
@sort-change="handleSortChange"
|
||||
@selection-change="handleSelectionChange"
|
||||
@pagination:size-change="handleSizeChange"
|
||||
@pagination:current-change="handleCurrentChange"
|
||||
>
|
||||
<!-- 操作列 -->
|
||||
<template #operation="{ row }">
|
||||
<div class="flex gap-2">
|
||||
<SaButton
|
||||
v-permission="'core:post:update'"
|
||||
type="secondary"
|
||||
@click="showDialog('edit', row)"
|
||||
/>
|
||||
<SaButton
|
||||
v-permission="'core:post:destroy'"
|
||||
type="error"
|
||||
@click="deleteRow(row, api.delete, refreshData)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</ArtTable>
|
||||
</ElCard>
|
||||
|
||||
<!-- 编辑弹窗 -->
|
||||
<EditDialog
|
||||
v-model="dialogVisible"
|
||||
:dialog-type="dialogType"
|
||||
:data="dialogData"
|
||||
@success="refreshData"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useTable } from '@/hooks/core/useTable'
|
||||
import { useSaiAdmin } from '@/composables/useSaiAdmin'
|
||||
import api from '@/api/system/post'
|
||||
import TableSearch from './modules/table-search.vue'
|
||||
import EditDialog from './modules/edit-dialog.vue'
|
||||
|
||||
// 搜索表单
|
||||
const searchForm = ref({
|
||||
name: undefined,
|
||||
code: undefined,
|
||||
status: undefined
|
||||
})
|
||||
|
||||
// 搜索处理
|
||||
const handleSearch = (params: Record<string, any>) => {
|
||||
Object.assign(searchParams, params)
|
||||
getData()
|
||||
}
|
||||
|
||||
// 表格配置
|
||||
const {
|
||||
columns,
|
||||
columnChecks,
|
||||
data,
|
||||
loading,
|
||||
getData,
|
||||
searchParams,
|
||||
pagination,
|
||||
resetSearchParams,
|
||||
handleSortChange,
|
||||
handleSizeChange,
|
||||
handleCurrentChange,
|
||||
refreshData
|
||||
} = useTable({
|
||||
core: {
|
||||
apiFn: api.list,
|
||||
columnsFactory: () => [
|
||||
{ type: 'selection' },
|
||||
{ prop: 'id', label: 'table.columns.common.no', width: 100, align: 'center' },
|
||||
{ prop: 'name', label: 'page.table.postName', minWidth: 120 },
|
||||
{ prop: 'code', label: 'page.table.postCode', minWidth: 120 },
|
||||
{ prop: 'remark', label: 'table.columns.common.description', minWidth: 150, showOverflowTooltip: true },
|
||||
{ prop: 'sort', label: 'page.table.sort', width: 100 },
|
||||
{ prop: 'status', label: 'page.table.status', saiType: 'dict', saiDict: 'data_status', width: 100 },
|
||||
{ prop: 'create_time', label: 'page.table.createTime', width: 180, sortable: true },
|
||||
{ prop: 'operation', label: 'table.actions.operation', width: 100, fixed: 'right', useSlot: true }
|
||||
]
|
||||
}
|
||||
})
|
||||
|
||||
// 编辑配置
|
||||
const {
|
||||
dialogType,
|
||||
dialogVisible,
|
||||
dialogData,
|
||||
showDialog,
|
||||
deleteRow,
|
||||
deleteSelectedRows,
|
||||
handleSelectionChange,
|
||||
selectedRows
|
||||
} = useSaiAdmin()
|
||||
</script>
|
||||
@@ -1,166 +0,0 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="visible"
|
||||
:title="dialogType === 'add' ? $t('page.form.titleAdd') : $t('page.form.titleEdit')"
|
||||
width="600px"
|
||||
align-center
|
||||
:close-on-click-modal="false"
|
||||
@close="handleClose"
|
||||
>
|
||||
<el-form ref="formRef" :model="formData" :rules="rules" label-width="120px">
|
||||
<el-form-item :label="$t('page.form.labelName')" prop="name">
|
||||
<el-input v-model="formData.name" :placeholder="$t('page.form.placeholderName')" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('page.form.labelCode')" prop="code">
|
||||
<el-input v-model="formData.code" :placeholder="$t('page.form.placeholderCode')" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('page.form.labelRemark')" prop="remark">
|
||||
<el-input
|
||||
v-model="formData.remark"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
:placeholder="$t('page.form.placeholderRemark')"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('page.form.labelSort')" prop="sort">
|
||||
<el-input-number v-model="formData.sort" :placeholder="$t('page.form.placeholderSort')" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('page.form.labelStatus')" prop="status">
|
||||
<sa-radio v-model="formData.status" dict="data_status" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="handleClose">{{ $t('common.cancel') }}</el-button>
|
||||
<el-button type="primary" @click="handleSubmit">{{ $t('table.form.submit') }}</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import api from '@/api/system/post'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
interface Props {
|
||||
modelValue: boolean
|
||||
dialogType: string
|
||||
data?: Record<string, any>
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:modelValue', value: boolean): void
|
||||
(e: 'success'): void
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
modelValue: false,
|
||||
dialogType: 'add',
|
||||
data: undefined
|
||||
})
|
||||
|
||||
const emit = defineEmits<Emits>()
|
||||
const { t } = useI18n()
|
||||
|
||||
const formRef = ref<FormInstance>()
|
||||
|
||||
/**
|
||||
* 弹窗显示状态双向绑定
|
||||
*/
|
||||
const visible = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (value) => emit('update:modelValue', value)
|
||||
})
|
||||
|
||||
/**
|
||||
* 表单验证规则
|
||||
*/
|
||||
const rules = computed<FormRules>(() => ({
|
||||
name: [{ required: true, message: t('page.form.ruleNameRequired'), trigger: 'blur' }],
|
||||
code: [{ required: true, message: t('page.form.ruleCodeRequired'), trigger: 'blur' }]
|
||||
}))
|
||||
|
||||
/**
|
||||
* 初始数据
|
||||
*/
|
||||
const initialFormData = {
|
||||
id: null,
|
||||
name: '',
|
||||
code: '',
|
||||
remark: '',
|
||||
sort: 100,
|
||||
status: 1
|
||||
}
|
||||
|
||||
/**
|
||||
* 表单数据
|
||||
*/
|
||||
const formData = reactive({ ...initialFormData })
|
||||
|
||||
/**
|
||||
* 监听弹窗打开,初始化表单数据
|
||||
*/
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(newVal) => {
|
||||
if (newVal) {
|
||||
initPage()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* 初始化页面数据
|
||||
*/
|
||||
const initPage = async () => {
|
||||
// 先重置为初始值
|
||||
Object.assign(formData, initialFormData)
|
||||
// 如果有数据,则填充数据
|
||||
if (props.data) {
|
||||
await nextTick()
|
||||
initForm()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化表单数据
|
||||
*/
|
||||
const initForm = () => {
|
||||
if (props.data) {
|
||||
for (const key in formData) {
|
||||
if (props.data[key] != null && props.data[key] != undefined) {
|
||||
;(formData as any)[key] = props.data[key]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭弹窗并重置表单
|
||||
*/
|
||||
const handleClose = () => {
|
||||
visible.value = false
|
||||
formRef.value?.resetFields()
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交表单
|
||||
*/
|
||||
const handleSubmit = async () => {
|
||||
if (!formRef.value) return
|
||||
try {
|
||||
await formRef.value.validate()
|
||||
if (props.dialogType === 'add') {
|
||||
await api.save(formData)
|
||||
ElMessage.success(t('page.form.addSuccess'))
|
||||
} else {
|
||||
await api.update(formData)
|
||||
ElMessage.success(t('page.form.editSuccess'))
|
||||
}
|
||||
emit('success')
|
||||
handleClose()
|
||||
} catch (error) {
|
||||
console.log('表单验证失败:', error)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -1,77 +0,0 @@
|
||||
<template>
|
||||
<sa-search-bar
|
||||
ref="searchBarRef"
|
||||
v-model="formData"
|
||||
label-width="100px"
|
||||
:showExpand="false"
|
||||
@reset="handleReset"
|
||||
@search="handleSearch"
|
||||
@expand="handleExpand"
|
||||
>
|
||||
<el-col v-bind="setSpan(6)">
|
||||
<el-form-item :label="$t('page.search.postName')" prop="name">
|
||||
<el-input v-model="formData.name" :placeholder="$t('page.search.placeholderPostName')" clearable />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col v-bind="setSpan(6)">
|
||||
<el-form-item :label="$t('page.search.postCode')" prop="code">
|
||||
<el-input v-model="formData.code" :placeholder="$t('page.search.placeholderPostCode')" clearable />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col v-bind="setSpan(6)">
|
||||
<el-form-item :label="$t('page.search.status')" prop="status">
|
||||
<sa-select v-model="formData.status" dict="data_status" :placeholder="$t('page.search.searchSelectPlaceholder')" clearable />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</sa-search-bar>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
interface Props {
|
||||
modelValue: Record<string, any>
|
||||
}
|
||||
interface Emits {
|
||||
(e: 'update:modelValue', value: Record<string, any>): void
|
||||
(e: 'search', params: Record<string, any>): void
|
||||
(e: 'reset'): void
|
||||
}
|
||||
const props = defineProps<Props>()
|
||||
const emit = defineEmits<Emits>()
|
||||
// 展开/收起
|
||||
const isExpanded = ref<boolean>(false)
|
||||
|
||||
// 表单数据双向绑定
|
||||
const searchBarRef = ref()
|
||||
const formData = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (val) => emit('update:modelValue', val)
|
||||
})
|
||||
|
||||
// 重置
|
||||
function handleReset() {
|
||||
searchBarRef.value?.ref.resetFields()
|
||||
emit('reset')
|
||||
}
|
||||
|
||||
// 搜索
|
||||
async function handleSearch() {
|
||||
emit('search', formData.value)
|
||||
}
|
||||
|
||||
// 展开/收起
|
||||
function handleExpand(expanded: boolean) {
|
||||
isExpanded.value = expanded
|
||||
}
|
||||
|
||||
// 栅格占据的列数
|
||||
const setSpan = (span: number) => {
|
||||
return {
|
||||
span: span,
|
||||
xs: 24, // 手机:满宽显示
|
||||
sm: span >= 12 ? span : 12, // 平板:大于等于12保持,否则用半宽
|
||||
md: span >= 8 ? span : 8, // 中等屏幕:大于等于8保持,否则用三分之一宽
|
||||
lg: span,
|
||||
xl: span
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -125,6 +125,10 @@
|
||||
} = useTable({
|
||||
core: {
|
||||
apiFn: api.list,
|
||||
apiParams: {
|
||||
orderField: 'level',
|
||||
orderType: 'desc'
|
||||
},
|
||||
columnsFactory: () => [
|
||||
{ prop: 'id', label: 'table.columns.common.no', minWidth: 60, align: 'center' },
|
||||
{ prop: 'name', label: 'page.table.roleName', minWidth: 120 },
|
||||
|
||||
@@ -41,6 +41,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import api from '@/api/system/role'
|
||||
import { withChannelDeptParams } from '@/composables/useChannelDeptScope'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
@@ -155,11 +156,12 @@
|
||||
if (!formRef.value) return
|
||||
try {
|
||||
await formRef.value.validate()
|
||||
const payload = withChannelDeptParams({ ...formData })
|
||||
if (props.dialogType === 'add') {
|
||||
await api.save(formData)
|
||||
await api.save(payload)
|
||||
ElMessage.success(t('page.form.addSuccess'))
|
||||
} else {
|
||||
await api.update(formData)
|
||||
await api.update(payload)
|
||||
ElMessage.success(t('page.form.editSuccess'))
|
||||
}
|
||||
emit('success')
|
||||
|
||||
@@ -1,16 +1,24 @@
|
||||
<template>
|
||||
<div class="art-full-height">
|
||||
<div class="box-border flex gap-4 h-full max-md:block max-md:gap-0 max-md:h-auto">
|
||||
<div class="flex-shrink-0 w-64 h-full max-md:w-full max-md:h-auto max-md:mb-5">
|
||||
<ElCard class="tree-card art-card-xs flex flex-col h-full mt-0" shadow="never">
|
||||
<div
|
||||
v-show="showChannelSidebar"
|
||||
class="flex-shrink-0 w-64 h-full max-md:w-full max-md:h-auto max-md:mb-5"
|
||||
>
|
||||
<ElCard
|
||||
class="tree-card art-card-xs flex flex-col h-full mt-0"
|
||||
shadow="never"
|
||||
v-loading="channelTreeLoading"
|
||||
>
|
||||
<template #header>
|
||||
<b>部门列表</b>
|
||||
<b>{{ $t('page.ui.channelList') }}</b>
|
||||
</template>
|
||||
<ElScrollbar>
|
||||
<ElTree
|
||||
:data="treeData"
|
||||
:props="{ children: 'children', label: 'label' }"
|
||||
node-key="id"
|
||||
:current-node-key="currentChannelId"
|
||||
default-expand-all
|
||||
highlight-current
|
||||
@node-click="handleNodeClick"
|
||||
@@ -136,10 +144,19 @@
|
||||
import WorkDialog from './modules/work-dialog.vue'
|
||||
import api from '@/api/system/user'
|
||||
import deptApi from '@/api/system/dept'
|
||||
import { isSuperAdminUser } from '@/utils/channelLayout'
|
||||
|
||||
const userStore = useUserStore()
|
||||
|
||||
const treeData = ref([])
|
||||
interface ChannelTreeNode {
|
||||
id: number
|
||||
label: string
|
||||
children?: ChannelTreeNode[]
|
||||
}
|
||||
|
||||
const treeData = ref<ChannelTreeNode[]>([])
|
||||
const channelTreeLoading = ref(false)
|
||||
const currentChannelId = ref<number | undefined>(undefined)
|
||||
|
||||
// 编辑框
|
||||
const { dialogType, dialogVisible, dialogData, showDialog, handleSelectionChange, deleteRow } =
|
||||
@@ -228,24 +245,79 @@
|
||||
const handleReset = () => {
|
||||
searchForm.value.dept_id = undefined
|
||||
resetSearchParams()
|
||||
if (isSuperAdminUser()) {
|
||||
currentChannelId.value = undefined
|
||||
} else {
|
||||
applyDefaultChannelSelection()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换部门
|
||||
* @param data
|
||||
*/
|
||||
const handleNodeClick = (data: any) => {
|
||||
const handleNodeClick = (data: ChannelTreeNode) => {
|
||||
currentChannelId.value = data.id
|
||||
searchParams.dept_id = data.id
|
||||
getData()
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取部门数据
|
||||
*/
|
||||
const getDeptList = () => {
|
||||
deptApi.accessDept().then((data: any) => {
|
||||
treeData.value = data
|
||||
/** 仅超管显示左侧渠道列表;渠道管理员固定本渠道,由后端过滤 */
|
||||
const showChannelSidebar = computed(() => isSuperAdminUser())
|
||||
|
||||
const normalizeChannelTree = (list: unknown[]): ChannelTreeNode[] => {
|
||||
if (!Array.isArray(list)) {
|
||||
return []
|
||||
}
|
||||
return list
|
||||
.map((item) => {
|
||||
const row = item as Record<string, unknown>
|
||||
const id = Number(row.id ?? row.value ?? 0)
|
||||
const label = String(row.label ?? row.name ?? '')
|
||||
return { id, label }
|
||||
})
|
||||
.filter((node) => node.id > 0 && node.label !== '')
|
||||
}
|
||||
|
||||
const fallbackChannelTree = (): ChannelTreeNode[] => {
|
||||
const dept = userStore.info?.department
|
||||
if (!dept || Number(dept.id) <= 0) {
|
||||
return []
|
||||
}
|
||||
return [
|
||||
{
|
||||
id: Number(dept.id),
|
||||
label: String(dept.name ?? '')
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
const applyDefaultChannelSelection = () => {
|
||||
if (treeData.value.length === 0 || isSuperAdminUser()) {
|
||||
return
|
||||
}
|
||||
const first = treeData.value[0]
|
||||
currentChannelId.value = first.id
|
||||
searchParams.dept_id = first.id
|
||||
getData()
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取可操作渠道(渠道管理员至少展示本渠道)
|
||||
*/
|
||||
const getDeptList = async () => {
|
||||
channelTreeLoading.value = true
|
||||
try {
|
||||
const data = await deptApi.accessDept()
|
||||
const nodes = normalizeChannelTree(Array.isArray(data) ? data : [])
|
||||
treeData.value = nodes.length > 0 ? nodes : fallbackChannelTree()
|
||||
applyDefaultChannelSelection()
|
||||
} catch {
|
||||
treeData.value = fallbackChannelTree()
|
||||
applyDefaultChannelSelection()
|
||||
} finally {
|
||||
channelTreeLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -283,6 +355,10 @@
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (isSuperAdminUser()) {
|
||||
getDeptList()
|
||||
} else {
|
||||
applyDefaultChannelSelection()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -52,13 +52,14 @@
|
||||
<el-row>
|
||||
<el-col :span="12">
|
||||
<el-form-item :label="$t('page.form.labelDept')" prop="dept_id">
|
||||
<el-tree-select
|
||||
v-model="formData.dept_id"
|
||||
:data="optionData.deptData"
|
||||
:render-after-expand="false"
|
||||
check-strictly
|
||||
clearable
|
||||
<el-select v-model="formData.dept_id" clearable filterable>
|
||||
<el-option
|
||||
v-for="item in optionData.deptData"
|
||||
:key="item.id"
|
||||
:label="item.label"
|
||||
:value="item.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
@@ -76,18 +77,6 @@
|
||||
</el-row>
|
||||
|
||||
<el-row>
|
||||
<el-col :span="12">
|
||||
<el-form-item :label="$t('page.form.labelPost')" prop="post_ids">
|
||||
<el-select v-model="formData.post_ids" multiple clearable>
|
||||
<el-option
|
||||
v-for="post in optionData.postList"
|
||||
:key="(post as any)?.id"
|
||||
:value="(post as any)?.id"
|
||||
:label="(post as any)?.name"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item :label="$t('page.form.labelGender')" prop="gender">
|
||||
<sa-radio v-model="formData.gender" dict="gender" valueType="string" />
|
||||
@@ -129,7 +118,6 @@
|
||||
import api from '@/api/system/user'
|
||||
import deptApi from '@/api/system/dept'
|
||||
import roleApi from '@/api/system/role'
|
||||
import postApi from '@/api/system/post'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
@@ -156,8 +144,7 @@
|
||||
const formRef = ref<FormInstance>()
|
||||
const optionData = reactive({
|
||||
deptData: <any>[],
|
||||
roleList: <any>[],
|
||||
postList: <any>[]
|
||||
roleList: <any>[]
|
||||
})
|
||||
|
||||
/**
|
||||
@@ -207,7 +194,6 @@
|
||||
phone: '',
|
||||
email: '',
|
||||
role_ids: [],
|
||||
post_ids: [],
|
||||
status: 1,
|
||||
gender: '',
|
||||
remark: ''
|
||||
@@ -221,6 +207,41 @@
|
||||
/**
|
||||
* 监听弹窗打开,初始化表单数据
|
||||
*/
|
||||
const flattenDeptOptions = (list: any[], result: { id: number; label: string }[] = []) => {
|
||||
for (const item of list) {
|
||||
const id = item.id ?? item.value
|
||||
if (id !== undefined && id !== null) {
|
||||
result.push({
|
||||
id,
|
||||
label: String(item.label ?? item.name ?? id)
|
||||
})
|
||||
}
|
||||
if (item.children?.length) {
|
||||
flattenDeptOptions(item.children, result)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
const loadRoleOptions = async () => {
|
||||
const deptId = formData.dept_id
|
||||
const params =
|
||||
deptId !== undefined && deptId !== null && deptId !== ''
|
||||
? { dept_id: deptId }
|
||||
: undefined
|
||||
const roleData = await roleApi.accessRole(params)
|
||||
optionData.roleList = Array.isArray(roleData) ? roleData : []
|
||||
}
|
||||
|
||||
watch(
|
||||
() => formData.dept_id,
|
||||
() => {
|
||||
if (props.modelValue) {
|
||||
void loadRoleOptions()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(newVal) => {
|
||||
@@ -229,33 +250,23 @@
|
||||
}
|
||||
}
|
||||
)
|
||||
// 初始化页面数据
|
||||
|
||||
const initPage = async () => {
|
||||
// 先重置为初始值
|
||||
Object.assign(formData, initialFormData)
|
||||
// 部门数据
|
||||
const deptData = await deptApi.accessDept()
|
||||
optionData.deptData = deptData
|
||||
optionData.deptData = flattenDeptOptions(Array.isArray(deptData) ? deptData : [])
|
||||
// 角色数据
|
||||
const roleData = await roleApi.accessRole()
|
||||
optionData.roleList = roleData
|
||||
// 岗位数据
|
||||
const postData = await postApi.accessPost()
|
||||
optionData.postList = postData
|
||||
// 如果有数据,则填充数据
|
||||
if (props.data) {
|
||||
if (props.data?.id) {
|
||||
await nextTick()
|
||||
if (props.data.id) {
|
||||
let data = await api.read(props.data.id)
|
||||
if (data.postList) {
|
||||
const post = (data.postList as any[])?.map((item: any) => item.id)
|
||||
data.post_ids = post
|
||||
}
|
||||
const data = await api.read(props.data.id)
|
||||
const role = (data.roleList as any[])?.map((item: any) => item.id)
|
||||
data.role_ids = role
|
||||
data.password = ''
|
||||
initForm(data)
|
||||
}
|
||||
await loadRoleOptions()
|
||||
} else {
|
||||
await loadRoleOptions()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
# 数据库配置
|
||||
|
||||
DB_TYPE=mysql
|
||||
DB_HOST=127.0.0.1
|
||||
DB_PORT=3306
|
||||
DB_NAME=dafuweng-v3
|
||||
DB_USER=dafuweng-v3
|
||||
DB_PASSWORD=tA6rciKLKxpFNGAm
|
||||
DB_PASSWORD=123456
|
||||
DB_PREFIX=
|
||||
DB_POOL_MAX=32
|
||||
DB_POOL_MIN=4
|
||||
@@ -28,14 +29,16 @@ WEBMAN_CHANNEL_LISTEN_HOST=0.0.0.0
|
||||
GAME_URL=dice-v3-game.h55555game.top
|
||||
|
||||
# API 鉴权与用户(可选,不填则用默认值)
|
||||
# 平台对接 /api/v1/* 请求头 api-key(必填,与对接方约定)
|
||||
API_KEY=
|
||||
# authToken 签名密钥(必填,与客户端约定,用于 signature 校验)
|
||||
API_AUTH_TOKEN_SECRET=xF75oK91TQj13s0UmNIr1NBWMWGfflNO
|
||||
API_AUTH_TOKEN_SECRET=
|
||||
# authToken 时间戳允许误差秒数,防重放,默认 300
|
||||
API_AUTH_TOKEN_TIME_TOLERANCE=300
|
||||
API_AUTH_TOKEN_EXP=86400
|
||||
# API_USER_TOKEN_EXP=604800
|
||||
API_USER_CACHE_EXPIRE=86400
|
||||
API_USER_ENCRYPT_KEY=Wj818SK8dhKBKNOY3PUTmZfhQDMCXEZi
|
||||
API_USER_ENCRYPT_KEY=
|
||||
|
||||
# 验证码配置,支持cache|session
|
||||
CAPTCHA_MODE=cache
|
||||
|
||||
@@ -10,6 +10,7 @@ use support\think\Db;
|
||||
use app\api\logic\GameLogic;
|
||||
use app\api\logic\PlayStartLogic;
|
||||
use app\api\util\ReturnCode;
|
||||
use app\dice\helper\AdminScopeHelper;
|
||||
use app\dice\model\config\DiceConfig;
|
||||
use app\dice\model\ante_config\DiceAnteConfig;
|
||||
use app\dice\model\play_record\DicePlayRecord;
|
||||
@@ -34,7 +35,12 @@ class GameController extends BaseController
|
||||
*/
|
||||
public function config(Request $request): Response
|
||||
{
|
||||
$rows = DiceConfig::select('name', 'group', 'title', 'title_en', 'value', 'value_en', 'create_time', 'update_time')->get();
|
||||
$configDeptId = $this->resolvePlayerConfigDeptIdFromRequest($request);
|
||||
$rows = (new DiceConfig())
|
||||
->field('name,group,title,title_en,value,value_en,create_time,update_time')
|
||||
->where('dept_id', $configDeptId)
|
||||
->select()
|
||||
->toArray();
|
||||
$lang = $request->header('lang', 'zh');
|
||||
if (!is_string($lang) || $lang === '') {
|
||||
$lang = 'zh';
|
||||
@@ -43,15 +49,15 @@ class GameController extends BaseController
|
||||
$isEn = $langLower === 'en' || str_starts_with($langLower, 'en-');
|
||||
$data = [];
|
||||
foreach ($rows as $row) {
|
||||
$group = $row->group ?? '';
|
||||
$group = $row['group'] ?? '';
|
||||
if (!isset($data[$group])) {
|
||||
$data[$group] = [];
|
||||
}
|
||||
$title = $row->title;
|
||||
$value = $row->value;
|
||||
$title = $row['title'] ?? '';
|
||||
$value = $row['value'] ?? '';
|
||||
if ($isEn) {
|
||||
$titleEn = $row->title_en ?? '';
|
||||
$valueEn = $row->value_en ?? '';
|
||||
$titleEn = $row['title_en'] ?? '';
|
||||
$valueEn = $row['value_en'] ?? '';
|
||||
if ($titleEn !== '') {
|
||||
$title = $titleEn;
|
||||
}
|
||||
@@ -60,11 +66,11 @@ class GameController extends BaseController
|
||||
}
|
||||
}
|
||||
$data[$group][] = [
|
||||
'name' => $row->name,
|
||||
'name' => $row['name'] ?? '',
|
||||
'title' => $title,
|
||||
'value' => $value,
|
||||
'create_time' => $row->create_time,
|
||||
'update_time' => $row->update_time,
|
||||
'create_time' => $row['create_time'] ?? '',
|
||||
'update_time' => $row['update_time'] ?? '',
|
||||
];
|
||||
}
|
||||
return $this->success($data);
|
||||
@@ -107,7 +113,8 @@ class GameController extends BaseController
|
||||
*/
|
||||
public function lotteryPool(Request $request): Response
|
||||
{
|
||||
$list = DiceRewardConfig::getCachedList();
|
||||
$configDeptId = $this->resolvePlayerConfigDeptIdFromRequest($request);
|
||||
$list = DiceRewardConfig::getCachedList($configDeptId);
|
||||
$list = array_values(array_filter($list, function ($row) {
|
||||
return (string) ($row['tier'] ?? '') !== 'BIGWIN';
|
||||
}));
|
||||
@@ -145,9 +152,9 @@ class GameController extends BaseController
|
||||
*/
|
||||
public function anteConfig(Request $request): Response
|
||||
{
|
||||
// 用于后续抽奖校验:在接口中实例化 model,后续逻辑可复用相同的数据读取方式。
|
||||
$configDeptId = $this->resolvePlayerConfigDeptIdFromRequest($request);
|
||||
$anteConfigModel = new DiceAnteConfig();
|
||||
$rows = $anteConfigModel->order('id', 'asc')->select()->toArray();
|
||||
$rows = $anteConfigModel->where('dept_id', $configDeptId)->order('id', 'asc')->select()->toArray();
|
||||
return $this->success($rows);
|
||||
}
|
||||
|
||||
@@ -200,7 +207,8 @@ class GameController extends BaseController
|
||||
$rewardTier = array_key_exists('reward_tier', $data) ? (string) ($data['reward_tier'] ?? '') : '';
|
||||
$targetIndex = array_key_exists('target_index', $data) ? (int) ($data['target_index'] ?? 0) : 0;
|
||||
if ($rewardTier !== 'BIGWIN' && $targetIndex > 0) {
|
||||
$configRow = DiceRewardConfig::getCachedById($targetIndex);
|
||||
$configDeptId = AdminScopeHelper::resolvePlayerConfigDeptId($player);
|
||||
$configRow = DiceRewardConfig::getCachedById($targetIndex, $configDeptId);
|
||||
if ($configRow !== null) {
|
||||
$uiText = '';
|
||||
$uiTextEn = '';
|
||||
@@ -236,14 +244,19 @@ class GameController extends BaseController
|
||||
$timeoutRecord = null;
|
||||
$timeout_message = '';
|
||||
$adminId = null;
|
||||
$timeoutDeptId = null;
|
||||
try {
|
||||
$timeoutPlayer = DicePlayer::find($userId);
|
||||
$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 $_) {
|
||||
}
|
||||
try {
|
||||
$timeoutRecord = DicePlayRecord::create([
|
||||
'player_id' => $userId,
|
||||
'dept_id' => $timeoutDeptId,
|
||||
'admin_id' => $adminId,
|
||||
'lottery_config_id' => 0,
|
||||
'lottery_type' => 0,
|
||||
@@ -273,4 +286,20 @@ class GameController extends BaseController
|
||||
Db::execute('SELECT RELEASE_LOCK(?)', [$lockName]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 token 注入的 player_id 解析所属渠道配置 ID
|
||||
*/
|
||||
private function resolvePlayerConfigDeptIdFromRequest(Request $request): int
|
||||
{
|
||||
$userId = (int) ($request->player_id ?? 0);
|
||||
if ($userId <= 0) {
|
||||
return AdminScopeHelper::DEFAULT_TEMPLATE_DEPT;
|
||||
}
|
||||
$player = DicePlayer::find($userId);
|
||||
if (!$player) {
|
||||
return AdminScopeHelper::DEFAULT_TEMPLATE_DEPT;
|
||||
}
|
||||
return AdminScopeHelper::resolvePlayerConfigDeptId($player);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ namespace app\api\controller\v1;
|
||||
use app\api\cache\AuthTokenCache;
|
||||
use app\api\controller\BaseController;
|
||||
use app\api\util\ReturnCode;
|
||||
use plugin\saiadmin\app\model\system\SystemUser;
|
||||
use support\Request;
|
||||
use support\Response;
|
||||
use Tinywan\Jwt\JwtToken;
|
||||
@@ -54,6 +55,14 @@ class AuthTokenController extends BaseController
|
||||
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);
|
||||
$tokenResult = JwtToken::generateToken([
|
||||
'id' => 0,
|
||||
|
||||
@@ -7,7 +7,6 @@ use app\api\logic\UserLogic;
|
||||
use app\api\util\ReturnCode;
|
||||
use app\dice\model\game\DiceGame;
|
||||
use app\dice\model\player\DicePlayer;
|
||||
use plugin\saiadmin\app\model\system\SystemUser;
|
||||
use app\dice\model\play_record\DicePlayRecord;
|
||||
use app\dice\model\player_wallet_record\DicePlayerWalletRecord;
|
||||
use app\dice\model\player_ticket_record\DicePlayerTicketRecord;
|
||||
@@ -60,7 +59,7 @@ class GameController extends BaseController
|
||||
public function getGameList(Request $request): Response
|
||||
{
|
||||
$lang = $this->resolveLang($request->post('lang', 'zh'));
|
||||
$games = $this->buildPublicGameList($lang);
|
||||
$games = $this->buildPublicGameList($lang, $this->agentDeptId($request));
|
||||
return $this->success([
|
||||
'game_list' => $games,
|
||||
]);
|
||||
@@ -73,7 +72,7 @@ class GameController extends BaseController
|
||||
public function getGameHall(Request $request): Response
|
||||
{
|
||||
$lang = $this->resolveLang($request->post('lang', 'zh'));
|
||||
$games = $this->buildPublicGameList($lang);
|
||||
$games = $this->buildPublicGameList($lang, $this->agentDeptId($request));
|
||||
$hallUrl = '';
|
||||
if (!empty($games)) {
|
||||
$hallUrl = $games[0]['hall_url'] ?? '';
|
||||
@@ -93,36 +92,26 @@ class GameController extends BaseController
|
||||
public function getGameUrl(Request $request): Response
|
||||
{
|
||||
$username = trim((string) ($request->post('username', '')));
|
||||
$password = trim((string) ($request->post('password', '123456')));
|
||||
$time = trim((string) ($request->post('time', '')));
|
||||
|
||||
if ($username === '') {
|
||||
return $this->fail('username is required', ReturnCode::PARAMS_ERROR);
|
||||
}
|
||||
if ($password === '') {
|
||||
$password = '123456';
|
||||
}
|
||||
if ($time === '') {
|
||||
$time = (string) time();
|
||||
}
|
||||
|
||||
$adminId = null;
|
||||
$adminIdsInTopDept = null;
|
||||
$agentId = trim((string) ($request->agent_id ?? ''));
|
||||
if ($agentId !== '') {
|
||||
$systemUser = SystemUser::where('agent_id', $agentId)->find();
|
||||
if ($systemUser) {
|
||||
$adminId = (int) $systemUser->id;
|
||||
$adminIdsInTopDept = UserLogic::getAdminIdsByAgentIdTopDept($agentId);
|
||||
}
|
||||
}
|
||||
$deptId = $this->agentDeptId($request);
|
||||
$adminId = $this->agentAdminId($request);
|
||||
$adminIdsInTopDept = UserLogic::getAdminIdsByAgentIdTopDept(trim((string) ($request->agent_id ?? '')));
|
||||
|
||||
$lang = trim((string) ($request->post('lang', 'zh')));
|
||||
$lang = in_array($lang, ['en', 'zh'], true) ? $lang : 'zh';
|
||||
|
||||
try {
|
||||
$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) {
|
||||
return $this->fail($e->getMessage(), ReturnCode::PARAMS_ERROR);
|
||||
}
|
||||
@@ -145,24 +134,36 @@ class GameController extends BaseController
|
||||
{
|
||||
$usernameRaw = $request->input('username', '');
|
||||
$username = is_string($usernameRaw) ? trim($usernameRaw) : '';
|
||||
$deptId = $this->agentDeptId($request);
|
||||
|
||||
if ($username === '') {
|
||||
return $this->fail('username is required', ReturnCode::PARAMS_ERROR);
|
||||
}
|
||||
|
||||
$cached = UserCache::getPlayerInfoSnapshotByUsername($username);
|
||||
$cached = UserCache::getPlayerInfoSnapshotByUsername($this->scopedUsername($deptId, $username));
|
||||
if ($cached !== null) {
|
||||
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) {
|
||||
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'];
|
||||
$info = $player->hidden($hidden)->toArray();
|
||||
UserCache::setPlayerInfoSnapshotByUsername($username, $info);
|
||||
UserCache::setPlayerInfoSnapshotByUsername($this->scopedUsername($deptId, $username), $info);
|
||||
|
||||
return $this->success($info);
|
||||
}
|
||||
@@ -276,6 +277,7 @@ class GameController extends BaseController
|
||||
public function getPlayerGameRecord(Request $request): Response
|
||||
{
|
||||
$username = trim((string) ($request->post('username', '')));
|
||||
$deptId = $this->agentDeptId($request);
|
||||
$startCreateTime = trim((string) ($request->post('start_create_time', '')));
|
||||
$endCreateTime = trim((string) ($request->post('end_create_time', '')));
|
||||
$window = $this->resolvePullRecordTimeWindow($startCreateTime, $endCreateTime);
|
||||
@@ -284,10 +286,10 @@ class GameController extends BaseController
|
||||
}
|
||||
$limit = $this->resolvePullRecordLimit($request);
|
||||
|
||||
$query = DicePlayRecord::order('id', 'desc');
|
||||
$query = DicePlayRecord::where('dept_id', $deptId)->order('id', 'desc');
|
||||
|
||||
if ($username !== '') {
|
||||
$player = DicePlayer::where('username', $username)->find();
|
||||
$player = $this->findPlayerByUsername($username, $deptId);
|
||||
if (!$player) {
|
||||
return $this->success([]);
|
||||
}
|
||||
@@ -300,7 +302,7 @@ class GameController extends BaseController
|
||||
$list = $query->limit($limit)->select()->toArray();
|
||||
$playerIds = array_unique(array_column($list, 'player_id'));
|
||||
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 = [];
|
||||
foreach ($players as $p) {
|
||||
$playerMap[(int) ($p['id'] ?? 0)] = $p;
|
||||
@@ -321,6 +323,7 @@ class GameController extends BaseController
|
||||
public function getPlayerWalletRecord(Request $request): Response
|
||||
{
|
||||
$username = trim((string) ($request->post('username', '')));
|
||||
$deptId = $this->agentDeptId($request);
|
||||
$startCreateTime = trim((string) ($request->post('start_create_time', '')));
|
||||
$endCreateTime = trim((string) ($request->post('end_create_time', '')));
|
||||
$window = $this->resolvePullRecordTimeWindow($startCreateTime, $endCreateTime);
|
||||
@@ -329,10 +332,10 @@ class GameController extends BaseController
|
||||
}
|
||||
$limit = $this->resolvePullRecordLimit($request);
|
||||
|
||||
$query = DicePlayerWalletRecord::order('id', 'desc');
|
||||
$query = DicePlayerWalletRecord::where('dept_id', $deptId)->order('id', 'desc');
|
||||
|
||||
if ($username !== '') {
|
||||
$player = DicePlayer::where('username', $username)->find();
|
||||
$player = $this->findPlayerByUsername($username, $deptId);
|
||||
if (!$player) {
|
||||
return $this->success([]);
|
||||
}
|
||||
@@ -357,6 +360,7 @@ class GameController extends BaseController
|
||||
public function getPlayerTicketRecord(Request $request): Response
|
||||
{
|
||||
$username = trim((string) ($request->post('username', '')));
|
||||
$deptId = $this->agentDeptId($request);
|
||||
$startCreateTime = trim((string) ($request->post('start_create_time', '')));
|
||||
$endCreateTime = trim((string) ($request->post('end_create_time', '')));
|
||||
$window = $this->resolvePullRecordTimeWindow($startCreateTime, $endCreateTime);
|
||||
@@ -365,10 +369,10 @@ class GameController extends BaseController
|
||||
}
|
||||
$limit = $this->resolvePullRecordLimit($request);
|
||||
|
||||
$query = DicePlayerTicketRecord::order('id', 'desc');
|
||||
$query = DicePlayerTicketRecord::where('dept_id', $deptId)->order('id', 'desc');
|
||||
|
||||
if ($username !== '') {
|
||||
$player = DicePlayer::where('username', $username)->find();
|
||||
$player = $this->findPlayerByUsername($username, $deptId);
|
||||
if (!$player) {
|
||||
return $this->success([]);
|
||||
}
|
||||
@@ -394,6 +398,7 @@ class GameController extends BaseController
|
||||
public function setPlayerWallet(Request $request): Response
|
||||
{
|
||||
$username = trim((string) ($request->post('username', '')));
|
||||
$deptId = $this->agentDeptId($request);
|
||||
$coin = $request->post('coin');
|
||||
|
||||
if ($username === '') {
|
||||
@@ -408,9 +413,9 @@ class GameController extends BaseController
|
||||
return $this->fail('coin cannot be 0', ReturnCode::PARAMS_ERROR);
|
||||
}
|
||||
|
||||
$player = DicePlayer::where('username', $username)->find();
|
||||
$player = $this->findPlayerByUsername($username, $deptId);
|
||||
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);
|
||||
@@ -430,6 +435,7 @@ class GameController extends BaseController
|
||||
|
||||
$adminId = ($player->admin_id ?? null) ? (int) $player->admin_id : null;
|
||||
$record = DicePlayerWalletRecord::create([
|
||||
'dept_id' => $deptId,
|
||||
'player_id' => (int) $player->id,
|
||||
'admin_id' => $adminId,
|
||||
'coin' => $coinVal,
|
||||
@@ -452,6 +458,7 @@ class GameController extends BaseController
|
||||
UserCache::deleteUser($player->id);
|
||||
if ($player->username !== '') {
|
||||
UserCache::deletePlayerByUsername($player->username);
|
||||
UserCache::deletePlayerByUsername($this->scopedUsername($deptId, (string) $player->username));
|
||||
}
|
||||
|
||||
$recordArr = $record->toArray();
|
||||
@@ -471,13 +478,14 @@ class GameController extends BaseController
|
||||
return $langValue;
|
||||
}
|
||||
|
||||
private function buildPublicGameList(string $lang): array
|
||||
private function buildPublicGameList(string $lang, int $deptId): array
|
||||
{
|
||||
$rows = DiceGame::where('status', 1)
|
||||
->orderBy('sort', 'asc')
|
||||
->orderBy('id', 'asc')
|
||||
->select(array_merge(self::GAME_PUBLIC_FIELDS, ['game_name', 'game_name_en']))
|
||||
->get()
|
||||
->where('dept_id', $deptId)
|
||||
->order('sort', 'asc')
|
||||
->order('id', 'asc')
|
||||
->field(array_merge(self::GAME_PUBLIC_FIELDS, ['game_name', 'game_name_en']))
|
||||
->select()
|
||||
->toArray();
|
||||
if (empty($rows)) {
|
||||
return [];
|
||||
@@ -495,4 +503,26 @@ class GameController extends BaseController
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,4 +36,26 @@ return [
|
||||
'BATCH_DELETE_FORBIDDEN' => 'Batch delete is not allowed',
|
||||
'SUPER_ADMIN_CANNOT_DELETE' => 'Super admin cannot be deleted',
|
||||
'OLD_PASSWORD_WRONG' => 'Old password is incorrect',
|
||||
'ADD_SUCCESS' => 'Added successfully',
|
||||
'UPDATE_SUCCESS' => 'Updated successfully',
|
||||
'DELETE_SUCCESS' => 'Deleted successfully',
|
||||
'ADD_FAILED' => 'Add failed',
|
||||
'UPDATE_FAILED' => 'Update failed',
|
||||
'DELETE_FAILED' => 'Delete failed',
|
||||
'NOT_FOUND' => 'Data not found',
|
||||
'ANTE_MUST_POSITIVE' => 'Ante must be greater than 0',
|
||||
'ANTE_NOT_ALLOWED' => 'Ante %s is not available for current channel, please select from ante config',
|
||||
'ANTE_CONFIG_NOT_FOUND' => 'Ante config not found',
|
||||
'ANTE_CONFIG_NOT_IN_CHANNEL' => 'Ante config does not belong to current channel',
|
||||
'POOL_CONFIG_NOT_IN_CHANNEL' => 'Pool config does not belong to current channel',
|
||||
'CHANNEL_DEPT_ID_REQUIRED' => 'Please select a channel, or assign a valid administrator/player for this record',
|
||||
'INVALID_CHANNEL_DEPT_ID' => 'Invalid channel. Please reselect channel or administrator',
|
||||
'PLAYER_USERNAME_DEPT_UNIQUE' => 'Username already exists in this channel',
|
||||
'NO_PERMISSION_UPDATE' => 'No permission to update this record',
|
||||
'NO_PERMISSION_VIEW' => 'No permission to view this record',
|
||||
'NO_PERMISSION_OPERATE_PLAYER' => 'No permission to operate this player',
|
||||
'PLEASE_SELECT_DATA' => 'Please select data to delete',
|
||||
'OPERATION_SUCCESS' => 'Operation successful',
|
||||
'TEST_DATA_CLEARED' => 'Test data cleared',
|
||||
'CLEAR_FAILED' => 'Clear failed: %s',
|
||||
];
|
||||
|
||||
@@ -9,6 +9,28 @@ declare(strict_types=1);
|
||||
return [
|
||||
'success' => 'Success',
|
||||
'fail' => 'Fail',
|
||||
'add success' => 'Added successfully',
|
||||
'update success' => 'Updated successfully',
|
||||
'save success' => 'Saved successfully',
|
||||
'delete success' => 'Deleted successfully',
|
||||
'add failed' => 'Add failed',
|
||||
'update failed' => 'Update failed',
|
||||
'delete failed' => 'Delete failed',
|
||||
'not found' => 'Data not found',
|
||||
'operation success' => 'Operation successful',
|
||||
'test data cleared' => 'Test data cleared',
|
||||
'ante must be greater than 0' => 'Ante must be greater than 0',
|
||||
'ante not allowed: %s' => 'Ante %s is not available for current channel, please select from ante config',
|
||||
'pool config does not belong to current channel' => 'Pool config does not belong to current channel',
|
||||
'no permission to update this record' => 'No permission to update this record',
|
||||
'no permission to view this record' => 'No permission to view this record',
|
||||
'no permission to operate this player' => 'No permission to operate this player',
|
||||
'please select data to delete' => 'Please select data to delete',
|
||||
'please select player' => 'Please select player',
|
||||
'please login first' => 'Please login first',
|
||||
'missing player_id' => 'Missing player_id',
|
||||
'Player not found' => 'Player not found',
|
||||
'record not found' => 'Record not found',
|
||||
'username、password 不能为空' => 'username and password are required',
|
||||
'请携带 token' => 'Please provide token',
|
||||
'token 无效' => 'Invalid or expired token',
|
||||
@@ -26,6 +48,9 @@ return [
|
||||
'没有原因' => 'Unknown reason',
|
||||
'缺少参数: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_KEY' => 'API_KEY is not configured',
|
||||
'请携带 api-key' => 'Please provide api-key',
|
||||
'api-key 无效' => 'Invalid api-key',
|
||||
'密钥错误' => 'Invalid secret',
|
||||
'时间戳已过期或无效,请同步时间' => 'Timestamp expired or invalid, please sync time',
|
||||
'签名验证失败' => 'Signature verification failed',
|
||||
|
||||
@@ -36,5 +36,27 @@ return [
|
||||
'BATCH_DELETE_FORBIDDEN' => '禁止批量删除操作',
|
||||
'SUPER_ADMIN_CANNOT_DELETE' => '超级管理员禁止删除',
|
||||
'OLD_PASSWORD_WRONG' => '原密码错误',
|
||||
'ADD_SUCCESS' => '添加成功',
|
||||
'UPDATE_SUCCESS' => '修改成功',
|
||||
'DELETE_SUCCESS' => '删除成功',
|
||||
'ADD_FAILED' => '添加失败',
|
||||
'UPDATE_FAILED' => '修改失败',
|
||||
'DELETE_FAILED' => '删除失败',
|
||||
'NOT_FOUND' => '数据不存在',
|
||||
'ANTE_MUST_POSITIVE' => '底注必须大于 0',
|
||||
'ANTE_NOT_ALLOWED' => '底注 %s 在当前渠道不可用,请从底注配置中选择',
|
||||
'ANTE_CONFIG_NOT_FOUND' => '底注配置不存在',
|
||||
'ANTE_CONFIG_NOT_IN_CHANNEL' => '底注配置不属于当前渠道',
|
||||
'POOL_CONFIG_NOT_IN_CHANNEL' => '奖池配置不属于当前渠道',
|
||||
'CHANNEL_DEPT_ID_REQUIRED' => '请选择所属渠道,或为记录指定有效的所属管理员/玩家',
|
||||
'INVALID_CHANNEL_DEPT_ID' => '渠道无效,请重新选择所属渠道或管理员',
|
||||
'PLAYER_USERNAME_DEPT_UNIQUE' => '该渠道下用户名已存在',
|
||||
'NO_PERMISSION_UPDATE' => '无权限修改该记录',
|
||||
'NO_PERMISSION_VIEW' => '无权限查看该记录',
|
||||
'NO_PERMISSION_OPERATE_PLAYER' => '无权限操作该玩家',
|
||||
'PLEASE_SELECT_DATA' => '请选择要删除的数据',
|
||||
'OPERATION_SUCCESS' => '操作成功',
|
||||
'TEST_DATA_CLEARED' => '测试数据已清空',
|
||||
'CLEAR_FAILED' => '清空失败:%s',
|
||||
];
|
||||
|
||||
|
||||
@@ -70,10 +70,14 @@ class GameLogic
|
||||
UserCache::setUser($playerId, $updatedUserArr);
|
||||
|
||||
$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 {
|
||||
Db::transaction(function () use (
|
||||
$player,
|
||||
$playerId,
|
||||
$playerDeptId,
|
||||
$adminId,
|
||||
$cost,
|
||||
$coinBefore,
|
||||
@@ -93,6 +97,7 @@ class GameLogic
|
||||
|
||||
DicePlayerWalletRecord::create([
|
||||
'player_id' => $playerId,
|
||||
'dept_id' => $playerDeptId,
|
||||
'admin_id' => $adminId,
|
||||
'coin' => round(-$cost, 2),
|
||||
'type' => self::WALLET_TYPE_BUY_DRAW,
|
||||
@@ -106,6 +111,7 @@ class GameLogic
|
||||
|
||||
DicePlayerTicketRecord::create([
|
||||
'player_id' => $playerId,
|
||||
'dept_id' => $playerDeptId,
|
||||
'admin_id' => $adminId,
|
||||
'use_coins' => round($cost, 2),
|
||||
'ante' => 1,
|
||||
|
||||
@@ -6,6 +6,7 @@ namespace app\api\logic;
|
||||
use app\api\cache\UserCache;
|
||||
use app\api\util\ApiLang;
|
||||
use app\api\service\LotteryService;
|
||||
use app\dice\helper\AdminScopeHelper;
|
||||
use app\dice\model\lottery_pool_config\DiceLotteryPoolConfig;
|
||||
use app\dice\model\play_record\DicePlayRecord;
|
||||
use app\dice\model\ante_config\DiceAnteConfig;
|
||||
@@ -49,6 +50,8 @@ class PlayStartLogic
|
||||
private const SUPER_WIN_GRID_NUMBERS = [5, 10, 15, 20, 25, 30];
|
||||
/** 5 和 30 抽到即豹子,不参与 BIGWIN 权重判定;10/15/20/25 按 BIGWIN weight 判定是否豹子 */
|
||||
private const SUPER_WIN_ALWAYS_GRID_NUMBERS = [5, 30];
|
||||
/** T4 惩罚格余额不足时写入游玩记录的备注 */
|
||||
private const REMARK_T4_INSUFFICIENT_BALANCE = '惩罚格奖励:玩家余额不足,已扣尽钱包剩余余额';
|
||||
|
||||
/**
|
||||
* 执行一局游戏
|
||||
@@ -64,6 +67,11 @@ class PlayStartLogic
|
||||
throw new ApiException('User not found');
|
||||
}
|
||||
|
||||
$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;
|
||||
if ($ante <= 0) {
|
||||
throw new ApiException('ante must be a positive integer');
|
||||
@@ -71,7 +79,7 @@ class PlayStartLogic
|
||||
|
||||
// 注数合规校验:ante 必须存在于 dice_ante_config.mult
|
||||
$anteConfigModel = new DiceAnteConfig();
|
||||
$exists = $anteConfigModel->where('mult', $ante)->count();
|
||||
$exists = $anteConfigModel->where('mult', $ante)->where('dept_id', $configDeptId)->count();
|
||||
if ($exists <= 0) {
|
||||
throw new ApiException('当前注数不合规,请选择正确的注数');
|
||||
}
|
||||
@@ -109,8 +117,9 @@ class PlayStartLogic
|
||||
}
|
||||
}
|
||||
|
||||
$configType0 = DiceLotteryPoolConfig::where('name', 'default')->find();
|
||||
$configType1 = DiceLotteryPoolConfig::where('name', 'killScore')->find();
|
||||
$configType0 = DiceLotteryPoolConfig::where('name', 'default')->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) {
|
||||
throw new ApiException('Lottery pool config not found (name=default required)');
|
||||
}
|
||||
@@ -118,44 +127,43 @@ class PlayStartLogic
|
||||
// 付费抽奖:开始前扣除费用 ante * UNIT_COST
|
||||
$paidAmount = $ticketType === self::LOTTERY_TYPE_PAID ? round($ante * self::UNIT_COST, 2) : 0.0;
|
||||
|
||||
// 游玩前余额校验(按 T4 惩罚最大值兜底):
|
||||
// 门槛 = paidAmount(压注*1) + abs(T4最小real_ev)*ante
|
||||
$t4List = DiceRewardConfig::getCachedByTier('T4');
|
||||
$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) {
|
||||
// 付费抽奖:余额不足单局费用(ante * UNIT_COST)时不允许开始;惩罚格不足部分在局内扣尽剩余余额
|
||||
if ($ticketType === self::LOTTERY_TYPE_PAID && $coin < $paidAmount) {
|
||||
throw new ApiException('余额不足');
|
||||
}
|
||||
|
||||
// 彩金池累计盈利:用于判断是否触发杀分(不再依赖单个玩家累计盈利)
|
||||
// 该值来自 dice_lottery_pool_config.profit_amount
|
||||
// 该值来自 dice_lottery_pool_config.profit_amount(default 奖池)
|
||||
$poolProfitTotal = $configType0->profit_amount ?? 0;
|
||||
$safetyLine = (int) ($configType0->safety_line ?? 0);
|
||||
$killEnabled = ((int) ($configType0->kill_enabled ?? 1)) === 1;
|
||||
// 盈利>=安全线且开启杀分:付费/免费都用 killScore;盈利<安全线:付费用玩家权重,免费用 killScore(无则用 default)
|
||||
// 记录 lottery_config_id:用池权重时记对应池,付费用玩家权重时记 default
|
||||
$usePoolWeights = ($ticketType === self::LOTTERY_TYPE_PAID && $killEnabled && $poolProfitTotal >= $safetyLine && $configType1 !== null)
|
||||
|| ($ticketType === self::LOTTERY_TYPE_FREE);
|
||||
$config = $usePoolWeights
|
||||
? (($ticketType === self::LOTTERY_TYPE_FREE && $configType1 === null) ? $configType0 : $configType1)
|
||||
: $configType0;
|
||||
|
||||
$usePaidKill = $ticketType === self::LOTTERY_TYPE_PAID
|
||||
&& $killEnabled
|
||||
&& $poolProfitTotal >= $safetyLine
|
||||
&& $configKill !== null;
|
||||
|
||||
$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
|
||||
$rewardInstance = DiceReward::getCachedInstance();
|
||||
$rewardInstance = DiceReward::getCachedInstance($configDeptId);
|
||||
$byTierDirection = $rewardInstance['by_tier_direction'] ?? [];
|
||||
$maxTierRetry = 10;
|
||||
$chosen = null;
|
||||
@@ -197,9 +205,9 @@ class PlayStartLogic
|
||||
$rollNumber = (int) ($chosen['grid_number'] ?? 0);
|
||||
$realEv = (float) ($chosen['real_ev'] ?? 0);
|
||||
// T5/再来一次:以奖励行 tier 为准,并以摇奖档位 $tier 兜底(与 reward_tier 展示一致,避免 dice_reward 行缺 tier 时不发券)
|
||||
$isTierT5 = (string) ($chosen['tier'] ?? '') === 'T5';
|
||||
if ($isTierT5 === false && (string) ($tier ?? '') === 'T5') {
|
||||
$isTierT5 = true;
|
||||
$isTierPlayAgain = (string) ($chosen['tier'] ?? '') === 'T5';
|
||||
if ($isTierPlayAgain === false && (string) ($tier ?? '') === 'T5') {
|
||||
$isTierPlayAgain = true;
|
||||
}
|
||||
// 摇色子中奖:按 dice_reward_config.real_ev 直接结算(已乘 ante)
|
||||
$rewardWinCoin = round($realEv * $ante, 2);
|
||||
@@ -216,7 +224,7 @@ class PlayStartLogic
|
||||
$superWinCoin = 0.0;
|
||||
$rollArray = $this->generateNonSuperWinRollArrayWithSum($rollNumber);
|
||||
} else {
|
||||
$bigWinConfig = DiceRewardConfig::getCachedByTierAndGridNumber('BIGWIN', $rollNumber);
|
||||
$bigWinConfig = DiceRewardConfig::getCachedByTierAndGridNumber('BIGWIN', $rollNumber, $configDeptId);
|
||||
$alwaysSuperWin = in_array($rollNumber, self::SUPER_WIN_ALWAYS_GRID_NUMBERS, true);
|
||||
$doSuperWin = $alwaysSuperWin;
|
||||
if (!$doSuperWin) {
|
||||
@@ -240,7 +248,7 @@ class PlayStartLogic
|
||||
// 中 BIGWIN 豹子:不走原奖励流程,不记录原奖励,不触发 T5 再来一次,仅发放豹子奖金
|
||||
$rewardWinCoin = 0.0;
|
||||
$realEv = 0.0;
|
||||
$isTierT5 = false;
|
||||
$isTierPlayAgain = false;
|
||||
} else {
|
||||
$rollArray = $this->generateNonSuperWinRollArrayWithSum($rollNumber);
|
||||
}
|
||||
@@ -259,7 +267,11 @@ class PlayStartLogic
|
||||
$winCoin = round($superWinCoin + $rewardWinCoin, 2); // 赢取平台币 = 中大奖 + 摇色子中奖(豹子时 rewardWinCoin 已为 0)
|
||||
|
||||
$record = null;
|
||||
$settledWinCoin = $winCoin;
|
||||
$configId = (int) $config->id;
|
||||
if ($ticketType === self::LOTTERY_TYPE_PAID && !$usePaidKill && $playerLinkedPool !== null) {
|
||||
$configId = (int) $playerLinkedPool->id;
|
||||
}
|
||||
$type0ConfigId = (int) $configType0->id;
|
||||
$rewardId = ($isWin === 1 && $superWinCoin > 0) ? 0 : $targetIndex; // 中豹子不记录原奖励配置 id
|
||||
$configName = (string) ($config->name ?? '');
|
||||
@@ -267,6 +279,7 @@ class PlayStartLogic
|
||||
try {
|
||||
Db::transaction(function () use (
|
||||
$playerId,
|
||||
$playerDeptId,
|
||||
$adminId,
|
||||
$configId,
|
||||
$type0ConfigId,
|
||||
@@ -284,30 +297,12 @@ class PlayStartLogic
|
||||
$startIndex,
|
||||
$targetIndex,
|
||||
$rollArray,
|
||||
$isTierT5,
|
||||
$isTierPlayAgain,
|
||||
$tier,
|
||||
&$record
|
||||
&$record,
|
||||
&$settledWinCoin
|
||||
) {
|
||||
$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);
|
||||
if (!$p) {
|
||||
@@ -316,10 +311,41 @@ class PlayStartLogic
|
||||
$coinBefore = (float) $p->coin;
|
||||
// 开始前先扣付费金额,再加中奖金额(免费抽奖 paid_amount=0)
|
||||
$coinAfter = round($coinBefore - $paidAmount + $winCoin, 2);
|
||||
// T4 惩罚兜底:扣完购券费用后若余额不足以承受本次惩罚(导致为负),统一按“余额不足”提示
|
||||
$recordWinCoin = $winCoin;
|
||||
$recordRewardWinCoin = $rewardWinCoin;
|
||||
$playRecordRemark = null;
|
||||
// T4 惩罚:扣完购券费用后若不足以支付全额惩罚,则扣尽钱包剩余余额并记录备注
|
||||
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;
|
||||
// 免费抽奖消耗:优先消耗 free_ticket.count,耗尽则清空 free_ticket;否则兼容旧 free_ticket_count
|
||||
if ($ticketType === self::LOTTERY_TYPE_FREE) {
|
||||
@@ -354,6 +380,7 @@ class PlayStartLogic
|
||||
$freeCnt = $isPaidPlay ? 0 : 1;
|
||||
DicePlayerTicketRecord::create([
|
||||
'player_id' => $playerId,
|
||||
'dept_id' => $playerDeptId,
|
||||
'admin_id' => $adminId,
|
||||
'use_coins' => $paidAmount,
|
||||
'ante' => $ante,
|
||||
@@ -366,7 +393,7 @@ class PlayStartLogic
|
||||
// 若本局中奖档位为 T5,则额外赠送 1 次免费抽奖次数:
|
||||
// - 新结构:写入 free_ticket(ante=本局注数,count+1)
|
||||
// - 兼容旧结构:free_ticket_count +1
|
||||
if ($isTierT5) {
|
||||
if ($isTierPlayAgain) {
|
||||
$ft = $p->free_ticket ?? null;
|
||||
$ftAnte = null;
|
||||
$ftCount = 0;
|
||||
@@ -392,6 +419,7 @@ class PlayStartLogic
|
||||
|
||||
DicePlayerTicketRecord::create([
|
||||
'player_id' => $playerId,
|
||||
'dept_id' => $playerDeptId,
|
||||
'admin_id' => $adminId,
|
||||
'ante' => $ante,
|
||||
'free_ticket_count' => 1,
|
||||
@@ -419,7 +447,7 @@ class PlayStartLogic
|
||||
// 彩金池累计盈利累加在 name=default 彩金池上:
|
||||
// 付费:每局按「本局赢取平台币 win_coin - 抽奖费用 paid_amount(ante*UNIT_COST)」
|
||||
// 免费券: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);
|
||||
try {
|
||||
DiceLotteryPoolConfig::where('id', $type0ConfigId)->update([
|
||||
@@ -438,6 +466,7 @@ class PlayStartLogic
|
||||
$walletAfterBuy = round($coinBefore - $paidAmount, 2);
|
||||
DicePlayerWalletRecord::create([
|
||||
'player_id' => $playerId,
|
||||
'dept_id' => $playerDeptId,
|
||||
'admin_id' => $adminId,
|
||||
'coin' => round(-$paidAmount, 2),
|
||||
'type' => self::WALLET_TYPE_BUY_DRAW,
|
||||
@@ -448,11 +477,15 @@ class PlayStartLogic
|
||||
}
|
||||
|
||||
$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([
|
||||
'player_id' => $playerId,
|
||||
'dept_id' => $playerDeptId,
|
||||
'admin_id' => $adminId,
|
||||
'coin' => $winCoin,
|
||||
'coin' => $recordWinCoin,
|
||||
'type' => self::WALLET_TYPE_DRAW,
|
||||
'wallet_before' => round($walletBeforeDraw, 2),
|
||||
'wallet_after' => $coinAfter,
|
||||
@@ -464,6 +497,7 @@ class PlayStartLogic
|
||||
try {
|
||||
$record = DicePlayRecord::create([
|
||||
'player_id' => $playerId,
|
||||
'dept_id' => $playerDeptId,
|
||||
'admin_id' => $adminId ?? null,
|
||||
'lottery_config_id' => $configId ?? 0,
|
||||
'lottery_type' => $ticketType,
|
||||
@@ -639,9 +673,10 @@ class PlayStartLogic
|
||||
* @param array|null $customTierWeights 自定义档位权重 ['T1'=>x, 'T2'=>x, ...],非空时忽略 config 的档位权重
|
||||
* @return array 可直接用于 DicePlayRecordTest::create 的字段 + tier(用于统计档位概率)
|
||||
*/
|
||||
public function simulateOnePlay($config, int $direction, int $lotteryType = 0, int $ante = 1, ?array $customTierWeights = null): array
|
||||
public function simulateOnePlay($config, int $direction, int $lotteryType = 0, int $ante = 1, ?array $customTierWeights = null, ?int $configDeptId = null): array
|
||||
{
|
||||
$rewardInstance = DiceReward::getCachedInstance();
|
||||
$useKillMode = false;
|
||||
$rewardInstance = DiceReward::getCachedInstance($configDeptId);
|
||||
$byTierDirection = $rewardInstance['by_tier_direction'] ?? [];
|
||||
$maxTierRetry = 10;
|
||||
$chosen = null;
|
||||
@@ -698,7 +733,7 @@ class PlayStartLogic
|
||||
$superWinCoin = 0.0;
|
||||
$rollArray = $this->generateNonSuperWinRollArrayWithSum($rollNumber);
|
||||
} else {
|
||||
$bigWinConfig = DiceRewardConfig::getCachedByTierAndGridNumber('BIGWIN', $rollNumber);
|
||||
$bigWinConfig = DiceRewardConfig::getCachedByTierAndGridNumber('BIGWIN', $rollNumber, $configDeptId);
|
||||
$alwaysSuperWin = in_array($rollNumber, self::SUPER_WIN_ALWAYS_GRID_NUMBERS, true);
|
||||
$doSuperWin = $alwaysSuperWin;
|
||||
if (!$doSuperWin) {
|
||||
@@ -763,4 +798,24 @@ class PlayStartLogic
|
||||
'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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ namespace app\api\logic;
|
||||
|
||||
use app\dice\model\player\DicePlayer;
|
||||
use app\api\cache\UserCache;
|
||||
use plugin\saiadmin\app\model\system\SystemDept;
|
||||
use plugin\saiadmin\app\model\system\SystemUser;
|
||||
use plugin\saiadmin\exception\ApiException;
|
||||
use Tinywan\Jwt\JwtToken;
|
||||
@@ -44,46 +43,8 @@ class UserLogic
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据 parent_id 向上遍历找到顶级部门(parent_id=0)
|
||||
*/
|
||||
private static function getTopDeptIdByParentId(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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据顶级部门 id,递归获取其下所有部门 id(含自身),仅用 id 和 parent_id
|
||||
*/
|
||||
private static function getAllDeptIdsUnderTop(int $topId): array
|
||||
{
|
||||
$deptIds = [$topId];
|
||||
$prevCount = 0;
|
||||
while (count($deptIds) > $prevCount) {
|
||||
$prevCount = count($deptIds);
|
||||
$children = SystemDept::whereIn('parent_id', $deptIds)->column('id');
|
||||
$deptIds = array_unique(array_merge($deptIds, array_map('intval', $children)));
|
||||
}
|
||||
return array_values($deptIds);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据 agent_id 获取当前管理员所在顶级部门下的所有管理员 ID 列表
|
||||
* 使用 SystemDept 的 id 和 parent_id 字段遍历:先向上找顶级部门(parent_id=0),再向下收集所有子部门
|
||||
* 用于 getGameUrl 接口判断 DicePlayer 是否属于该部门,同顶级部门下不重复创建玩家
|
||||
* 根据 agent_id 获取同渠道下的所有管理员 ID 列表
|
||||
* 用于 getGameUrl 接口判断 DicePlayer 是否属于该渠道,同渠道下不重复创建玩家
|
||||
*
|
||||
* @param string $agentId 代理标识(sa_system_user.agent_id)
|
||||
* @return int[] 管理员 ID 列表,空数组表示未找到或无法解析
|
||||
@@ -103,16 +64,56 @@ class UserLogic
|
||||
return [(int) $admin->id];
|
||||
}
|
||||
$deptId = (int) $deptId;
|
||||
$topId = self::getTopDeptIdByParentId($deptId);
|
||||
if ($topId === null) {
|
||||
return [(int) $admin->id];
|
||||
$adminIds = SystemUser::where('dept_id', $deptId)->column('id');
|
||||
return array_map('intval', $adminIds ?: [(int) $admin->id]);
|
||||
}
|
||||
$deptIds = self::getAllDeptIdsUnderTop($topId);
|
||||
if (empty($deptIds)) {
|
||||
$deptIds = [$deptId];
|
||||
|
||||
/**
|
||||
* 按用户名查找玩家;不存在则创建并绑定渠道/管理员(供 getPlayerInfo 等接口)
|
||||
*
|
||||
* @param int|null $adminId 关联后台管理员 ID(sa_system_user.id)
|
||||
* @param int|null $deptId 所属渠道 ID
|
||||
*/
|
||||
public function findOrCreatePlayerByUsername(string $username, ?int $adminId = null, ?int $deptId = null): DicePlayer
|
||||
{
|
||||
$username = trim($username);
|
||||
if ($username === '') {
|
||||
throw new ApiException('username is required');
|
||||
}
|
||||
$adminIds = SystemUser::whereIn('dept_id', $deptIds)->column('id');
|
||||
return array_map('intval', $adminIds);
|
||||
|
||||
$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;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -123,7 +124,7 @@ class UserLogic
|
||||
* @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): array
|
||||
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 === '') {
|
||||
@@ -131,6 +132,9 @@ class UserLogic
|
||||
}
|
||||
|
||||
$query = DicePlayer::where('username', $username);
|
||||
if ($deptId !== null && $deptId > 0) {
|
||||
$query->where('dept_id', $deptId);
|
||||
}
|
||||
if ($adminIdsInTopDept !== null && !empty($adminIdsInTopDept)) {
|
||||
$query->whereIn('admin_id', $adminIdsInTopDept);
|
||||
}
|
||||
@@ -139,10 +143,12 @@ class UserLogic
|
||||
if ((int) ($player->status ?? 1) === 0) {
|
||||
throw new ApiException('Account is disabled and cannot log in');
|
||||
}
|
||||
if (!$skipPasswordValidation) {
|
||||
$hashed = $this->hashPassword($password);
|
||||
if ($player->password !== $hashed) {
|
||||
throw new ApiException('Wrong password');
|
||||
}
|
||||
}
|
||||
$currentCoin = (float) $player->coin;
|
||||
$player->coin = $currentCoin + $coin;
|
||||
$player->save();
|
||||
@@ -153,8 +159,15 @@ class UserLogic
|
||||
$player->password = $this->hashPassword($password);
|
||||
$player->status = self::STATUS_NORMAL;
|
||||
$player->coin = $coin;
|
||||
if ($deptId !== null && $deptId > 0) {
|
||||
$player->dept_id = $deptId;
|
||||
}
|
||||
if ($adminId !== null && $adminId > 0) {
|
||||
$player->admin_id = $adminId;
|
||||
$adminUser = SystemUser::find($adminId);
|
||||
if (($deptId === null || $deptId <= 0) && $adminUser && !empty($adminUser->dept_id)) {
|
||||
$player->dept_id = $adminUser->dept_id;
|
||||
}
|
||||
}
|
||||
$player->save();
|
||||
}
|
||||
@@ -168,6 +181,7 @@ class UserLogic
|
||||
]);
|
||||
$token = $tokenResult['access_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();
|
||||
UserCache::setUser((int) $player->id, $userArr);
|
||||
|
||||
@@ -20,6 +20,7 @@ class ApiAccessLogMiddleware implements MiddlewareInterface
|
||||
|
||||
/** 请求头名称(小写) */
|
||||
private const SENSITIVE_HEADER_NAMES = [
|
||||
'api-key',
|
||||
'auth-token',
|
||||
'token',
|
||||
'authorization',
|
||||
@@ -32,6 +33,8 @@ class ApiAccessLogMiddleware implements MiddlewareInterface
|
||||
'secret',
|
||||
'signature',
|
||||
'token',
|
||||
'api-key',
|
||||
'api_key',
|
||||
'auth-token',
|
||||
'auth_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\util\ReturnCode;
|
||||
use plugin\saiadmin\app\model\system\SystemUser;
|
||||
use plugin\saiadmin\exception\ApiException;
|
||||
use Tinywan\Jwt\JwtToken;
|
||||
use Tinywan\Jwt\Exception\JwtTokenException;
|
||||
@@ -53,7 +54,17 @@ class AuthTokenMiddleware implements MiddlewareInterface
|
||||
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_admin_id = (int) $agent->id;
|
||||
$request->agent_dept_id = (int) $agent->dept_id;
|
||||
return $handler($request);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,10 +53,14 @@ class TokenMiddleware implements MiddlewareInterface
|
||||
if ($username === '') {
|
||||
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 === '') {
|
||||
$player = DicePlayer::where('username', $username)->find();
|
||||
$player = DicePlayer::find($userId);
|
||||
if (!$player) {
|
||||
throw new ApiException('Please register', ReturnCode::TOKEN_INVALID);
|
||||
}
|
||||
@@ -68,17 +72,17 @@ class TokenMiddleware implements MiddlewareInterface
|
||||
|
||||
// 优先从 Redis 缓存取玩家,避免每次请求都查库
|
||||
$player = null;
|
||||
$cached = UserCache::getPlayerByUsername($username);
|
||||
if ($cached !== null && isset($cached['id'])) {
|
||||
$cached = UserCache::getUser($userId);
|
||||
if (!empty($cached) && isset($cached['id']) && (int) $cached['id'] === $userId) {
|
||||
$player = (new DicePlayer())->data($cached, true);
|
||||
}
|
||||
if ($player === null) {
|
||||
$player = DicePlayer::where('username', $username)->find();
|
||||
$player = DicePlayer::find($userId);
|
||||
if (!$player) {
|
||||
UserCache::deleteSessionByUsername($username);
|
||||
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 = $player;
|
||||
|
||||
@@ -83,6 +83,47 @@ class LotteryService
|
||||
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 */
|
||||
public static function drawTierByWeights(DiceLotteryPoolConfig $config): string
|
||||
{
|
||||
|
||||
20
server/app/dice/basic/DiceBaseLogic.php
Normal file
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\dice\basic;
|
||||
|
||||
use plugin\saiadmin\basic\think\BaseLogic;
|
||||
|
||||
/**
|
||||
* 大富翁逻辑层基类:删除均为硬删除
|
||||
*/
|
||||
class DiceBaseLogic extends BaseLogic
|
||||
{
|
||||
/**
|
||||
* @param mixed $ids
|
||||
*/
|
||||
public function destroy($ids): bool
|
||||
{
|
||||
return $this->model->destroy($ids, true);
|
||||
}
|
||||
}
|
||||
@@ -7,8 +7,10 @@ use app\dice\helper\AdminScopeHelper;
|
||||
use app\dice\model\player\DicePlayer;
|
||||
use app\dice\model\player_wallet_record\DicePlayerWalletRecord;
|
||||
use app\dice\model\play_record\DicePlayRecord;
|
||||
use app\dice\model\reward\DiceRewardConfig;
|
||||
use plugin\saiadmin\basic\BaseController;
|
||||
use plugin\saiadmin\service\Permission;
|
||||
use support\Request;
|
||||
use support\Response;
|
||||
use support\think\Db;
|
||||
|
||||
@@ -21,53 +23,51 @@ class DiceDashboardController extends BaseController
|
||||
* 工作台卡片统计:玩家注册、充值、提现、游玩次数(含较上周对比)
|
||||
*/
|
||||
#[Permission('工作台数据统计', 'core:console:list')]
|
||||
public function statistics(): Response
|
||||
public function statistics(Request $request): Response
|
||||
{
|
||||
$thisWeekStart = date('Y-m-d 00:00:00', strtotime('monday this week'));
|
||||
$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'));
|
||||
[$thisStart, $thisEnd, $lastStart, $lastEnd] = $this->resolveDateRanges($request);
|
||||
|
||||
$adminInfo = $this->adminInfo ?? null;
|
||||
$filterDeptId = $request->input('dept_id');
|
||||
|
||||
$playerQueryThis = DicePlayer::whereBetween('create_time', [$thisWeekStart, $thisWeekEnd]);
|
||||
$playerQueryLast = DicePlayer::whereBetween('create_time', [$lastWeekStart, $lastWeekEnd]);
|
||||
AdminScopeHelper::applyAdminScope($playerQueryThis, $adminInfo);
|
||||
AdminScopeHelper::applyAdminScope($playerQueryLast, $adminInfo);
|
||||
$playerQueryThis = DicePlayer::whereBetween('create_time', [$thisStart, $thisEnd]);
|
||||
$playerQueryLast = DicePlayer::whereBetween('create_time', [$lastStart, $lastEnd]);
|
||||
AdminScopeHelper::applyAdminScope($playerQueryThis, $adminInfo, $filterDeptId);
|
||||
AdminScopeHelper::applyAdminScope($playerQueryLast, $adminInfo, $filterDeptId);
|
||||
$playerThis = $playerQueryThis->count();
|
||||
$playerLast = $playerQueryLast->count();
|
||||
|
||||
$chargeQueryThis = DicePlayerWalletRecord::where('type', 0)
|
||||
->where('coin', '>', 0)
|
||||
->whereBetween('create_time', [$thisWeekStart, $thisWeekEnd]);
|
||||
->whereBetween('create_time', [$thisStart, $thisEnd]);
|
||||
$chargeQueryLast = DicePlayerWalletRecord::where('type', 0)
|
||||
->where('coin', '>', 0)
|
||||
->whereBetween('create_time', [$lastWeekStart, $lastWeekEnd]);
|
||||
AdminScopeHelper::applyAdminScope($chargeQueryThis, $adminInfo);
|
||||
AdminScopeHelper::applyAdminScope($chargeQueryLast, $adminInfo);
|
||||
->whereBetween('create_time', [$lastStart, $lastEnd]);
|
||||
AdminScopeHelper::applyAdminScope($chargeQueryThis, $adminInfo, $filterDeptId);
|
||||
AdminScopeHelper::applyAdminScope($chargeQueryLast, $adminInfo, $filterDeptId);
|
||||
$chargeThis = $chargeQueryThis->sum('coin');
|
||||
$chargeLast = $chargeQueryLast->sum('coin');
|
||||
|
||||
$withdrawQueryThis = DicePlayerWalletRecord::where('type', 1)
|
||||
->whereBetween('create_time', [$thisWeekStart, $thisWeekEnd]);
|
||||
->whereBetween('create_time', [$thisStart, $thisEnd]);
|
||||
$withdrawQueryLast = DicePlayerWalletRecord::where('type', 1)
|
||||
->whereBetween('create_time', [$lastWeekStart, $lastWeekEnd]);
|
||||
AdminScopeHelper::applyAdminScope($withdrawQueryThis, $adminInfo);
|
||||
AdminScopeHelper::applyAdminScope($withdrawQueryLast, $adminInfo);
|
||||
->whereBetween('create_time', [$lastStart, $lastEnd]);
|
||||
AdminScopeHelper::applyAdminScope($withdrawQueryThis, $adminInfo, $filterDeptId);
|
||||
AdminScopeHelper::applyAdminScope($withdrawQueryLast, $adminInfo, $filterDeptId);
|
||||
$withdrawThis = $withdrawQueryThis->sum(Db::raw('ABS(coin)'));
|
||||
$withdrawLast = $withdrawQueryLast->sum(Db::raw('ABS(coin)'));
|
||||
|
||||
$playQueryThis = DicePlayRecord::whereBetween('create_time', [$thisWeekStart, $thisWeekEnd]);
|
||||
$playQueryLast = DicePlayRecord::whereBetween('create_time', [$lastWeekStart, $lastWeekEnd]);
|
||||
AdminScopeHelper::applyAdminScope($playQueryThis, $adminInfo);
|
||||
AdminScopeHelper::applyAdminScope($playQueryLast, $adminInfo);
|
||||
$playQueryThis = DicePlayRecord::whereBetween('create_time', [$thisStart, $thisEnd]);
|
||||
$playQueryLast = DicePlayRecord::whereBetween('create_time', [$lastStart, $lastEnd]);
|
||||
AdminScopeHelper::applyAdminScope($playQueryThis, $adminInfo, $filterDeptId);
|
||||
AdminScopeHelper::applyAdminScope($playQueryLast, $adminInfo, $filterDeptId);
|
||||
$playThis = $playQueryThis->count();
|
||||
$playLast = $playQueryLast->count();
|
||||
|
||||
$playerChange = $this->calcWeekChange($playerThis, $playerLast);
|
||||
$chargeChange = $this->calcWeekChange((float) $chargeThis, (float) $chargeLast);
|
||||
$withdrawChange = $this->calcWeekChange((float) $withdrawThis, (float) $withdrawLast);
|
||||
$playChange = $this->calcWeekChange($playThis, $playLast);
|
||||
$playerChange = $this->calcPeriodChange($playerThis, $playerLast);
|
||||
$chargeChange = $this->calcPeriodChange((float) $chargeThis, (float) $chargeLast);
|
||||
$withdrawChange = $this->calcPeriodChange((float) $withdrawThis, (float) $withdrawLast);
|
||||
$playChange = $this->calcPeriodChange($playThis, $playLast);
|
||||
|
||||
return $this->success([
|
||||
'player_count' => $playerThis,
|
||||
@@ -78,6 +78,7 @@ class DiceDashboardController extends BaseController
|
||||
'withdraw_amount_change' => $withdrawChange,
|
||||
'play_count' => $playThis,
|
||||
'play_count_change' => $playChange,
|
||||
'date' => $this->resolveRequestDate($request),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -85,13 +86,11 @@ class DiceDashboardController extends BaseController
|
||||
* 近期玩家充值统计(近10天每日充值金额)
|
||||
*/
|
||||
#[Permission('工作台数据统计', 'core:console:list')]
|
||||
public function rechargeChart(): Response
|
||||
public function rechargeChart(Request $request): Response
|
||||
{
|
||||
$adminInfo = $this->adminInfo ?? null;
|
||||
$allowedIds = AdminScopeHelper::getAllowedAdminIds($adminInfo);
|
||||
$adminCondition = '';
|
||||
if ($allowedIds !== null) {
|
||||
if (empty($allowedIds)) {
|
||||
$deptCondition = $this->buildWalletSqlDeptCondition($adminInfo, $request->input('dept_id'));
|
||||
if ($deptCondition === '__empty__') {
|
||||
$data = [];
|
||||
foreach (range(0, 9) as $n) {
|
||||
$data[] = ['recharge_date' => date('Y-m-d', strtotime("-{$n} days")), 'recharge_amount' => 0];
|
||||
@@ -102,9 +101,6 @@ class DiceDashboardController extends BaseController
|
||||
'recharge_date' => array_column($data, 'recharge_date'),
|
||||
]);
|
||||
}
|
||||
$idsStr = implode(',', array_map('intval', $allowedIds));
|
||||
$adminCondition = " AND w.admin_id IN ({$idsStr})";
|
||||
}
|
||||
$sql = "
|
||||
SELECT
|
||||
d.date AS recharge_date,
|
||||
@@ -116,7 +112,7 @@ class DiceDashboardController extends BaseController
|
||||
UNION ALL SELECT 7 UNION ALL SELECT 8 UNION ALL SELECT 9) a
|
||||
) d
|
||||
LEFT JOIN dice_player_wallet_record w
|
||||
ON DATE(w.create_time) = d.date AND w.type = 0 AND w.coin > 0 {$adminCondition}
|
||||
ON DATE(w.create_time) = d.date AND w.type = 0 AND w.coin > 0 {$deptCondition}
|
||||
GROUP BY d.date
|
||||
ORDER BY d.date ASC
|
||||
";
|
||||
@@ -131,13 +127,11 @@ class DiceDashboardController extends BaseController
|
||||
* 月度玩家充值汇总(当年1-12月每月充值金额)
|
||||
*/
|
||||
#[Permission('工作台数据统计', 'core:console:list')]
|
||||
public function rechargeBarChart(): Response
|
||||
public function rechargeBarChart(Request $request): Response
|
||||
{
|
||||
$adminInfo = $this->adminInfo ?? null;
|
||||
$allowedIds = AdminScopeHelper::getAllowedAdminIds($adminInfo);
|
||||
$adminCondition = '';
|
||||
if ($allowedIds !== null) {
|
||||
if (empty($allowedIds)) {
|
||||
$deptCondition = $this->buildWalletSqlDeptCondition($adminInfo, $request->input('dept_id'));
|
||||
if ($deptCondition === '__empty__') {
|
||||
$data = [];
|
||||
for ($m = 1; $m <= 12; $m++) {
|
||||
$data[] = ['recharge_month' => sprintf('%02d月', $m), 'recharge_amount' => 0];
|
||||
@@ -147,9 +141,6 @@ class DiceDashboardController extends BaseController
|
||||
'recharge_month' => array_column($data, 'recharge_month'),
|
||||
]);
|
||||
}
|
||||
$idsStr = implode(',', array_map('intval', $allowedIds));
|
||||
$adminCondition = " AND w.admin_id IN ({$idsStr})";
|
||||
}
|
||||
$sql = "
|
||||
SELECT
|
||||
CONCAT(LPAD(m.month_num, 2, '0'), '月') AS recharge_month,
|
||||
@@ -162,7 +153,7 @@ class DiceDashboardController extends BaseController
|
||||
LEFT JOIN dice_player_wallet_record w
|
||||
ON YEAR(w.create_time) = YEAR(CURDATE())
|
||||
AND MONTH(w.create_time) = m.month_num
|
||||
AND w.type = 0 AND w.coin > 0 {$adminCondition}
|
||||
AND w.type = 0 AND w.coin > 0 {$deptCondition}
|
||||
GROUP BY m.month_num
|
||||
ORDER BY m.month_num ASC
|
||||
";
|
||||
@@ -178,7 +169,7 @@ class DiceDashboardController extends BaseController
|
||||
* 返回:玩家账号(DicePlayer.username)、充值金额(coin)、充值时间(create_time)
|
||||
*/
|
||||
#[Permission('工作台数据统计', 'core:console:list')]
|
||||
public function walletRecordList(): Response
|
||||
public function walletRecordList(Request $request): Response
|
||||
{
|
||||
$adminInfo = $this->adminInfo ?? null;
|
||||
$query = DicePlayerWalletRecord::with([
|
||||
@@ -186,10 +177,11 @@ class DiceDashboardController extends BaseController
|
||||
$q->field('id,username');
|
||||
},
|
||||
])
|
||||
->where('type', 0)
|
||||
->order('create_time', 'desc')
|
||||
->where('type', 0);
|
||||
$this->applyDashboardDateFilter($query, $request);
|
||||
$query->order('create_time', 'desc')
|
||||
->limit(50);
|
||||
AdminScopeHelper::applyAdminScope($query, $adminInfo);
|
||||
AdminScopeHelper::applyAdminScope($query, $adminInfo, $request->input('dept_id'));
|
||||
$list = $query->select();
|
||||
$rows = [];
|
||||
foreach ($list as $row) {
|
||||
@@ -208,13 +200,14 @@ class DiceDashboardController extends BaseController
|
||||
* 返回:玩家账号(username)、余额(coin)、抽奖券(total_ticket_count)
|
||||
*/
|
||||
#[Permission('工作台数据统计', 'core:console:list')]
|
||||
public function newPlayerList(): Response
|
||||
public function newPlayerList(Request $request): Response
|
||||
{
|
||||
$adminInfo = $this->adminInfo ?? null;
|
||||
$query = DicePlayer::field('username,coin,total_ticket_count,create_time')
|
||||
->order('create_time', 'desc')
|
||||
$query = DicePlayer::field('username,coin,total_ticket_count,create_time');
|
||||
$this->applyDashboardDateFilter($query, $request);
|
||||
$query->order('create_time', 'desc')
|
||||
->limit(50);
|
||||
AdminScopeHelper::applyAdminScope($query, $adminInfo);
|
||||
AdminScopeHelper::applyAdminScope($query, $adminInfo, $request->input('dept_id'));
|
||||
$list = $query->select();
|
||||
$rows = [];
|
||||
foreach ($list as $row) {
|
||||
@@ -222,16 +215,141 @@ class DiceDashboardController extends BaseController
|
||||
'name' => $row->getAttr('username'),
|
||||
'coin' => $row->getAttr('coin'),
|
||||
'total_ticket_count' => $row->getAttr('total_ticket_count'),
|
||||
'create_time' => $row->getAttr('create_time'),
|
||||
];
|
||||
}
|
||||
return $this->success($rows);
|
||||
}
|
||||
|
||||
private function calcWeekChange($current, $last): float
|
||||
/**
|
||||
* 工作台-玩家游玩记录:最新50条
|
||||
* 返回:玩家账号、中奖档位、赢取平台币、游玩时间
|
||||
*/
|
||||
#[Permission('工作台数据统计', 'core:console:list')]
|
||||
public function playRecordList(Request $request): Response
|
||||
{
|
||||
$adminInfo = $this->adminInfo ?? null;
|
||||
$query = DicePlayRecord::with([
|
||||
'dicePlayer' => function ($q) {
|
||||
$q->field('id,username');
|
||||
},
|
||||
])
|
||||
->where('status', 1)
|
||||
->field('id,player_id,reward_tier,win_coin,create_time');
|
||||
$this->applyDashboardDateFilter($query, $request);
|
||||
$query->order('create_time', 'desc')
|
||||
->limit(50);
|
||||
AdminScopeHelper::applyAdminScope($query, $adminInfo, $request->input('dept_id'));
|
||||
$list = $query->select();
|
||||
$tierLabels = $this->buildRewardTierLabels();
|
||||
$rows = [];
|
||||
foreach ($list as $row) {
|
||||
$player = $row->dicePlayer;
|
||||
$tier = $row->getAttr('reward_tier');
|
||||
$rows[] = [
|
||||
'player_name' => $player ? $player->getAttr('username') : '',
|
||||
'reward_tier' => $tier,
|
||||
'reward_tier_label' => $tierLabels[$tier] ?? $tier,
|
||||
'win_coin' => $row->getAttr('win_coin'),
|
||||
'create_time' => $row->getAttr('create_time'),
|
||||
];
|
||||
}
|
||||
return $this->success($rows);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string>
|
||||
*/
|
||||
private function buildRewardTierLabels(): array
|
||||
{
|
||||
$rows = DiceRewardConfig::field('tier,ui_text')->select();
|
||||
$labels = [];
|
||||
foreach ($rows as $row) {
|
||||
$tier = $row->getAttr('tier');
|
||||
if ($tier === '' || $tier === null) {
|
||||
continue;
|
||||
}
|
||||
if (!isset($labels[$tier])) {
|
||||
$labels[$tier] = $row->getAttr('ui_text') ?: $tier;
|
||||
}
|
||||
}
|
||||
return $labels;
|
||||
}
|
||||
|
||||
private function calcPeriodChange($current, $last): float
|
||||
{
|
||||
if ($last == 0) {
|
||||
return $current > 0 ? 100.0 : 0.0;
|
||||
}
|
||||
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__
|
||||
*/
|
||||
private function buildWalletSqlDeptCondition(?array $adminInfo, $requestDeptId): string
|
||||
{
|
||||
if (AdminScopeHelper::getDeptId($adminInfo) !== null) {
|
||||
$deptId = AdminScopeHelper::getDeptId($adminInfo);
|
||||
if ($deptId <= 0) {
|
||||
return '__empty__';
|
||||
}
|
||||
return ' AND w.dept_id = ' . $deptId;
|
||||
}
|
||||
$target = AdminScopeHelper::resolveBusinessDeptId($adminInfo, $requestDeptId);
|
||||
if ($target !== null && $target > 0) {
|
||||
return ' AND w.dept_id = ' . $target;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,9 @@
|
||||
// +----------------------------------------------------------------------
|
||||
namespace app\dice\controller\ante_config;
|
||||
|
||||
use app\dice\helper\AdminScopeHelper;
|
||||
use app\dice\logic\ante_config\DiceAnteConfigLogic;
|
||||
use app\dice\model\ante_config\DiceAnteConfig;
|
||||
use app\dice\validate\ante_config\DiceAnteConfigValidate;
|
||||
use plugin\saiadmin\basic\BaseController;
|
||||
use plugin\saiadmin\service\Permission;
|
||||
@@ -34,10 +36,32 @@ class DiceAnteConfigController extends BaseController
|
||||
['is_default', ''],
|
||||
]);
|
||||
$query = $this->logic->search($where);
|
||||
AdminScopeHelper::applyConfigScope($query, $this->adminInfo ?? null, $request->input('dept_id'));
|
||||
$data = $this->logic->getList($query);
|
||||
return $this->success($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 底注下拉选项(按渠道),供一键测试权重等使用
|
||||
*/
|
||||
#[Permission('底注配置列表', 'dice:ante_config:index:index')]
|
||||
public function getOptions(Request $request): Response
|
||||
{
|
||||
$query = DiceAnteConfig::field('id,name,title,mult,is_default')->order('mult', 'asc')->order('id', 'asc');
|
||||
AdminScopeHelper::applyConfigScope($query, $this->adminInfo ?? null, $request->input('dept_id'));
|
||||
$list = $query->select();
|
||||
$data = $list->map(static function ($item) {
|
||||
return [
|
||||
'id' => (int) $item['id'],
|
||||
'name' => (string) ($item['name'] ?? ''),
|
||||
'title' => (string) ($item['title'] ?? ''),
|
||||
'mult' => (int) ($item['mult'] ?? 0),
|
||||
'is_default' => (int) ($item['is_default'] ?? 0),
|
||||
];
|
||||
})->toArray();
|
||||
return $this->success($data);
|
||||
}
|
||||
|
||||
#[Permission('底注配置读取', 'dice:ante_config:index:read')]
|
||||
public function read(Request $request): Response
|
||||
{
|
||||
@@ -52,6 +76,7 @@ class DiceAnteConfigController extends BaseController
|
||||
{
|
||||
$data = $request->post();
|
||||
$this->validate('save', $data);
|
||||
AdminScopeHelper::prepareConfigSaveData($data, $this->adminInfo ?? null, $request->input('dept_id'), $data);
|
||||
$result = $this->logic->add($data);
|
||||
return $result ? $this->success('add success') : $this->fail('add failed');
|
||||
}
|
||||
@@ -60,8 +85,19 @@ class DiceAnteConfigController extends BaseController
|
||||
public function update(Request $request): Response
|
||||
{
|
||||
$data = $request->post();
|
||||
if ($data === [] || $data === null) {
|
||||
$data = $request->all();
|
||||
}
|
||||
$this->validate('update', $data);
|
||||
$result = $this->logic->edit($data['id'], $data);
|
||||
$requestDeptId = AdminScopeHelper::pickRequestDeptId($request->input('dept_id'), is_array($data) ? $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, $requestDeptId)) {
|
||||
return $this->fail('no permission to update this record');
|
||||
}
|
||||
}
|
||||
$result = $this->logic->edit($data['id'], $data, $this->adminInfo ?? null, $requestDeptId);
|
||||
return $result ? $this->success('update success') : $this->fail('update failed');
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
// +----------------------------------------------------------------------
|
||||
namespace app\dice\controller\config;
|
||||
|
||||
use app\dice\helper\AdminScopeHelper;
|
||||
use plugin\saiadmin\basic\BaseController;
|
||||
use app\dice\logic\config\DiceConfigLogic;
|
||||
use app\dice\validate\config\DiceConfigValidate;
|
||||
@@ -42,6 +43,7 @@ class DiceConfigController extends BaseController
|
||||
['title', ''],
|
||||
]);
|
||||
$query = $this->logic->search($where);
|
||||
AdminScopeHelper::applyConfigScope($query, $this->adminInfo ?? null, $request->input('dept_id'));
|
||||
$data = $this->logic->getList($query);
|
||||
return $this->success($data);
|
||||
}
|
||||
@@ -74,6 +76,7 @@ class DiceConfigController extends BaseController
|
||||
{
|
||||
$data = $request->post();
|
||||
$this->validate('save', $data);
|
||||
AdminScopeHelper::prepareConfigSaveData($data, $this->adminInfo ?? null, $request->input('dept_id'), $data);
|
||||
$result = $this->logic->add($data);
|
||||
if ($result) {
|
||||
return $this->success('add success');
|
||||
@@ -91,8 +94,20 @@ class DiceConfigController extends BaseController
|
||||
public function update(Request $request): Response
|
||||
{
|
||||
$data = $request->post();
|
||||
if ($data === [] || $data === null) {
|
||||
$data = $request->all();
|
||||
}
|
||||
$this->validate('update', $data);
|
||||
$result = $this->logic->edit($data['id'], $data);
|
||||
$requestDeptId = AdminScopeHelper::pickRequestDeptId($request->input('dept_id'), is_array($data) ? $data : []);
|
||||
$deptId = AdminScopeHelper::resolveConfigDeptId($this->adminInfo ?? null, $requestDeptId);
|
||||
$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, $requestDeptId)) {
|
||||
return $this->fail('no permission to update this record');
|
||||
}
|
||||
}
|
||||
$result = $this->logic->edit($data['id'], $data, $this->adminInfo ?? null, $requestDeptId);
|
||||
if ($result) {
|
||||
return $this->success('update success');
|
||||
} else {
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
// +----------------------------------------------------------------------
|
||||
namespace app\dice\controller\game;
|
||||
|
||||
use app\dice\helper\AdminScopeHelper;
|
||||
use app\dice\logic\game\DiceGameLogic;
|
||||
use app\dice\validate\game\DiceGameValidate;
|
||||
use plugin\saiadmin\basic\BaseController;
|
||||
@@ -33,6 +34,7 @@ class DiceGameController extends BaseController
|
||||
['status', ''],
|
||||
]);
|
||||
$query = $this->logic->search($where);
|
||||
AdminScopeHelper::applyConfigScope($query, $this->adminInfo ?? null, $request->input('dept_id'));
|
||||
$data = $this->logic->getList($query);
|
||||
return $this->success($data);
|
||||
}
|
||||
@@ -54,6 +56,7 @@ class DiceGameController extends BaseController
|
||||
{
|
||||
$data = $request->post();
|
||||
$this->validate('save', $data);
|
||||
AdminScopeHelper::prepareConfigSaveData($data, $this->adminInfo ?? null, $request->input('dept_id'), $data);
|
||||
$result = $this->logic->add($data);
|
||||
if (!$result) {
|
||||
return $this->fail('add failed');
|
||||
@@ -65,8 +68,19 @@ class DiceGameController extends BaseController
|
||||
public function update(Request $request): Response
|
||||
{
|
||||
$data = $request->post();
|
||||
if ($data === [] || $data === null) {
|
||||
$data = $request->all();
|
||||
}
|
||||
$this->validate('update', $data);
|
||||
$result = $this->logic->edit($data['id'], $data);
|
||||
$requestDeptId = AdminScopeHelper::pickRequestDeptId($request->input('dept_id'), is_array($data) ? $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, $requestDeptId)) {
|
||||
return $this->fail('no permission to update this record');
|
||||
}
|
||||
}
|
||||
$result = $this->logic->edit($data['id'], $data, $this->adminInfo ?? null, $requestDeptId);
|
||||
if (!$result) {
|
||||
return $this->fail('update failed');
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
namespace app\dice\controller\lottery_pool_config;
|
||||
|
||||
use app\dice\model\lottery_pool_config\DiceLotteryPoolConfig;
|
||||
use app\dice\helper\AdminScopeHelper;
|
||||
use plugin\saiadmin\basic\BaseController;
|
||||
use app\dice\logic\lottery_pool_config\DiceLotteryPoolConfigLogic;
|
||||
use app\dice\validate\lottery_pool_config\DiceLotteryPoolConfigValidate;
|
||||
@@ -37,18 +38,23 @@ class DiceLotteryPoolConfigController extends BaseController
|
||||
#[Permission('色子奖池配置列表', 'dice:lottery_pool_config:index:index')]
|
||||
public function getOptions(Request $request): Response
|
||||
{
|
||||
$list = DiceLotteryPoolConfig::field('id,name,t1_weight,t2_weight,t3_weight,t4_weight,t5_weight')
|
||||
->order('id', 'asc')
|
||||
->select();
|
||||
$query = DiceLotteryPoolConfig::field('id,name,remark,t1_weight,t2_weight,t3_weight,t4_weight,t5_weight')
|
||||
->order('id', 'asc');
|
||||
AdminScopeHelper::applyConfigScope($query, $this->adminInfo ?? null, $request->input('dept_id'));
|
||||
$list = $query->select();
|
||||
$data = $list->map(function ($item) {
|
||||
$row = is_array($item) ? $item : $item->toArray();
|
||||
$display = DiceLotteryPoolConfig::displayLabel($row);
|
||||
return [
|
||||
'id' => (int) $item['id'],
|
||||
'name' => (string) ($item['name'] ?? ''),
|
||||
't1_weight' => (int) ($item['t1_weight'] ?? 0),
|
||||
't2_weight' => (int) ($item['t2_weight'] ?? 0),
|
||||
't3_weight' => (int) ($item['t3_weight'] ?? 0),
|
||||
't4_weight' => (int) ($item['t4_weight'] ?? 0),
|
||||
't5_weight' => (int) ($item['t5_weight'] ?? 0),
|
||||
'id' => (int) ($row['id'] ?? 0),
|
||||
'name' => (string) ($row['name'] ?? ''),
|
||||
'remark' => (string) ($row['remark'] ?? ''),
|
||||
'display_name' => $display,
|
||||
't1_weight' => (int) ($row['t1_weight'] ?? 0),
|
||||
't2_weight' => (int) ($row['t2_weight'] ?? 0),
|
||||
't3_weight' => (int) ($row['t3_weight'] ?? 0),
|
||||
't4_weight' => (int) ($row['t4_weight'] ?? 0),
|
||||
't5_weight' => (int) ($row['t5_weight'] ?? 0),
|
||||
];
|
||||
})->toArray();
|
||||
return $this->success($data);
|
||||
@@ -67,6 +73,7 @@ class DiceLotteryPoolConfigController extends BaseController
|
||||
['type', ''],
|
||||
]);
|
||||
$query = $this->logic->search($where);
|
||||
AdminScopeHelper::applyConfigScope($query, $this->adminInfo ?? null, $request->input('dept_id'));
|
||||
$data = $this->logic->getList($query);
|
||||
return $this->success($data);
|
||||
}
|
||||
@@ -99,6 +106,7 @@ class DiceLotteryPoolConfigController extends BaseController
|
||||
{
|
||||
$data = $request->post();
|
||||
$this->validate('save', $data);
|
||||
AdminScopeHelper::prepareConfigSaveData($data, $this->adminInfo ?? null, $request->input('dept_id'), $data);
|
||||
$result = $this->logic->add($data);
|
||||
if ($result) {
|
||||
return $this->success('add success');
|
||||
@@ -116,8 +124,19 @@ class DiceLotteryPoolConfigController extends BaseController
|
||||
public function update(Request $request): Response
|
||||
{
|
||||
$data = $request->post();
|
||||
if ($data === [] || $data === null) {
|
||||
$data = $request->all();
|
||||
}
|
||||
$this->validate('update', $data);
|
||||
$result = $this->logic->edit($data['id'], $data);
|
||||
$requestDeptId = AdminScopeHelper::pickRequestDeptId($request->input('dept_id'), is_array($data) ? $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, $requestDeptId)) {
|
||||
return $this->fail('no permission to update this record');
|
||||
}
|
||||
}
|
||||
$result = $this->logic->edit($data['id'], $data, $this->adminInfo ?? null, $requestDeptId);
|
||||
if ($result) {
|
||||
return $this->success('update success');
|
||||
} else {
|
||||
@@ -152,7 +171,8 @@ class DiceLotteryPoolConfigController extends BaseController
|
||||
#[Permission('色子奖池配置列表', 'dice:lottery_pool_config:index:getCurrentPool')]
|
||||
public function getCurrentPool(Request $request): Response
|
||||
{
|
||||
$data = $this->logic->getCurrentPool();
|
||||
$deptId = AdminScopeHelper::resolveConfigDeptId($this->adminInfo ?? null, $request->input('dept_id'));
|
||||
$data = $this->logic->getCurrentPool($deptId);
|
||||
return $this->success($data);
|
||||
}
|
||||
|
||||
@@ -163,7 +183,12 @@ class DiceLotteryPoolConfigController extends BaseController
|
||||
public function updateCurrentPool(Request $request): Response
|
||||
{
|
||||
$data = $request->post();
|
||||
$this->logic->updateCurrentPool($data);
|
||||
if ($data === [] || $data === null) {
|
||||
$data = $request->all();
|
||||
}
|
||||
$requestDeptId = AdminScopeHelper::pickRequestDeptId($request->input('dept_id'), is_array($data) ? $data : []);
|
||||
$deptId = AdminScopeHelper::resolveConfigDeptId($this->adminInfo ?? null, $requestDeptId);
|
||||
$this->logic->updateCurrentPool($data, $deptId);
|
||||
return $this->success('save success');
|
||||
}
|
||||
|
||||
@@ -173,7 +198,8 @@ class DiceLotteryPoolConfigController extends BaseController
|
||||
#[Permission('色子奖池配置修改', 'dice:lottery_pool_config:index:resetProfitAmount')]
|
||||
public function resetProfitAmount(Request $request): Response
|
||||
{
|
||||
$this->logic->resetProfitAmount();
|
||||
$deptId = AdminScopeHelper::resolveConfigDeptId($this->adminInfo ?? null, $request->input('dept_id'));
|
||||
$this->logic->resetProfitAmount($deptId);
|
||||
return $this->success('reset success');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,9 +51,11 @@ class DicePlayRecordController extends BaseController
|
||||
['reward_ui_text', ''],
|
||||
['reward_tier', ''],
|
||||
['direction', ''],
|
||||
['create_time_min', ''],
|
||||
['create_time_max', ''],
|
||||
]);
|
||||
$query = $this->logic->search($where);
|
||||
AdminScopeHelper::applyAdminScope($query, $this->adminInfo ?? null);
|
||||
AdminScopeHelper::applyAdminScope($query, $this->adminInfo ?? null, $request->input('dept_id'));
|
||||
$query->with([
|
||||
'dicePlayer',
|
||||
'diceLotteryPoolConfig',
|
||||
@@ -78,7 +80,7 @@ class DicePlayRecordController extends BaseController
|
||||
public function getPlayerOptions(Request $request): Response
|
||||
{
|
||||
$query = DicePlayer::field('id,username');
|
||||
AdminScopeHelper::applyAdminScope($query, $this->adminInfo ?? null);
|
||||
AdminScopeHelper::applyAdminScope($query, $this->adminInfo ?? null, $request->input('dept_id'));
|
||||
$list = $query->select();
|
||||
$data = $list->map(function ($item) {
|
||||
return ['id' => $item['id'], 'username' => $item['username'] ?? ''];
|
||||
@@ -92,9 +94,21 @@ class DicePlayRecordController extends BaseController
|
||||
#[Permission('玩家抽奖记录列表', 'dice:play_record:index:index')]
|
||||
public function getLotteryConfigOptions(Request $request): Response
|
||||
{
|
||||
$list = DiceLotteryPoolConfig::field('id,name')->select();
|
||||
$query = DiceLotteryPoolConfig::field('id,name,remark')->order('id', 'asc');
|
||||
$requestDeptId = AdminScopeHelper::pickRequestDeptId(
|
||||
$request->input('dept_id'),
|
||||
$request->all()
|
||||
);
|
||||
AdminScopeHelper::applyConfigScope($query, $this->adminInfo ?? null, $requestDeptId);
|
||||
$list = $query->select();
|
||||
$data = $list->map(function ($item) {
|
||||
return ['id' => $item['id'], 'name' => $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();
|
||||
return $this->success($data);
|
||||
}
|
||||
@@ -112,8 +126,7 @@ class DicePlayRecordController extends BaseController
|
||||
if (!$model) {
|
||||
return $this->fail('not found');
|
||||
}
|
||||
$allowedIds = AdminScopeHelper::getAllowedAdminIds($this->adminInfo ?? null);
|
||||
if ($allowedIds !== null && !in_array((int) ($model->admin_id ?? 0), $allowedIds, true)) {
|
||||
if (!AdminScopeHelper::canAccessDept($this->adminInfo ?? null, $model->dept_id ?? null)) {
|
||||
return $this->fail('no permission to view this record');
|
||||
}
|
||||
$data = is_array($model) ? $model : $model->toArray();
|
||||
@@ -130,6 +143,7 @@ class DicePlayRecordController extends BaseController
|
||||
{
|
||||
$data = $request->post();
|
||||
$this->validate('save', $data);
|
||||
AdminScopeHelper::prepareBusinessSaveData($data, $this->adminInfo ?? null, $request->input('dept_id'), $data);
|
||||
$result = $this->logic->add($data);
|
||||
if ($result) {
|
||||
return $this->success('add success');
|
||||
@@ -138,24 +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);
|
||||
$result = $this->logic->edit($data['id'], $data);
|
||||
if ($result) {
|
||||
return $this->success('update success');
|
||||
} else {
|
||||
return $this->fail('update failed');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除数据
|
||||
* @param Request $request
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
// +----------------------------------------------------------------------
|
||||
namespace app\dice\controller\play_record_test;
|
||||
|
||||
use app\dice\helper\AdminScopeHelper;
|
||||
use plugin\saiadmin\basic\BaseController;
|
||||
use app\dice\logic\play_record_test\DicePlayRecordTestLogic;
|
||||
use app\dice\validate\play_record_test\DicePlayRecordTestValidate;
|
||||
@@ -39,6 +40,7 @@ class DicePlayRecordTestController extends BaseController
|
||||
{
|
||||
$where = $request->more([
|
||||
['reward_config_record_id', ''],
|
||||
['lottery_config_id', ''],
|
||||
['lottery_type', ''],
|
||||
['direction', ''],
|
||||
['is_win', ''],
|
||||
@@ -50,6 +52,7 @@ class DicePlayRecordTestController extends BaseController
|
||||
['roll_number', ''],
|
||||
]);
|
||||
$query = $this->logic->search($where);
|
||||
AdminScopeHelper::applyAdminScope($query, $this->adminInfo ?? null, $request->input('dept_id'));
|
||||
$query->with(['diceLotteryPoolConfig']);
|
||||
|
||||
// 按当前筛选条件统计:平台总盈利 = 付费金额(paid_amount 求和) - 玩家总收益(win_coin 求和)
|
||||
@@ -92,6 +95,7 @@ class DicePlayRecordTestController extends BaseController
|
||||
{
|
||||
$data = $request->post();
|
||||
$this->validate('save', $data);
|
||||
AdminScopeHelper::prepareBusinessSaveData($data, $this->adminInfo ?? null, $request->input('dept_id'), $data);
|
||||
$result = $this->logic->add($data);
|
||||
if ($result) {
|
||||
return $this->success('add success');
|
||||
@@ -100,24 +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);
|
||||
$result = $this->logic->edit($data['id'], $data);
|
||||
if ($result) {
|
||||
return $this->success('update success');
|
||||
} else {
|
||||
return $this->fail('update failed');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除数据
|
||||
* @param Request $request
|
||||
|
||||
@@ -8,6 +8,7 @@ namespace app\dice\controller\player;
|
||||
|
||||
use app\dice\helper\AdminScopeHelper;
|
||||
use app\dice\model\lottery_pool_config\DiceLotteryPoolConfig;
|
||||
use plugin\saiadmin\app\model\system\SystemDept;
|
||||
use plugin\saiadmin\app\model\system\SystemUser;
|
||||
use plugin\saiadmin\basic\BaseController;
|
||||
use app\dice\logic\player\DicePlayerLogic;
|
||||
@@ -42,9 +43,21 @@ class DicePlayerController extends BaseController
|
||||
#[Permission('玩家列表', 'dice:player:index:index')]
|
||||
public function getLotteryConfigOptions(Request $request): Response
|
||||
{
|
||||
$list = DiceLotteryPoolConfig::field('id,name')->order('id', 'asc')->select();
|
||||
$query = DiceLotteryPoolConfig::field('id,name,remark')->order('id', 'asc');
|
||||
$requestDeptId = AdminScopeHelper::pickRequestDeptId(
|
||||
$request->input('dept_id'),
|
||||
$request->all()
|
||||
);
|
||||
AdminScopeHelper::applyConfigScope($query, $this->adminInfo ?? null, $requestDeptId);
|
||||
$list = $query->select();
|
||||
$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();
|
||||
return $this->success($data);
|
||||
}
|
||||
@@ -57,12 +70,17 @@ class DicePlayerController extends BaseController
|
||||
#[Permission('玩家列表', 'dice:player:index:index')]
|
||||
public function getSystemUserOptions(Request $request): Response
|
||||
{
|
||||
$query = SystemUser::field('id,username,realname')->where('status', 1)->order('id', 'asc');
|
||||
if (isset($this->adminInfo['id']) && (int) $this->adminInfo['id'] > 1) {
|
||||
$deptList = $this->adminInfo['deptList'] ?? [];
|
||||
if (!empty($deptList)) {
|
||||
$query->auth($deptList);
|
||||
$query = SystemUser::field('id,username,realname,dept_id')->where('status', 1)->order('id', 'asc');
|
||||
$requestDeptId = AdminScopeHelper::pickRequestDeptId(
|
||||
$request->input('dept_id'),
|
||||
$request->all()
|
||||
);
|
||||
$allowedIds = AdminScopeHelper::getAllowedAdminIds($this->adminInfo ?? null);
|
||||
if ($allowedIds !== null) {
|
||||
if ($allowedIds === []) {
|
||||
return $this->success([]);
|
||||
}
|
||||
$query->whereIn('id', $allowedIds);
|
||||
}
|
||||
$list = $query->select();
|
||||
$data = $list->map(function ($item) {
|
||||
@@ -71,12 +89,89 @@ class DicePlayerController extends BaseController
|
||||
'id' => (int) $item['id'],
|
||||
'username' => (string) ($item['username'] ?? ''),
|
||||
'realname' => (string) ($item['realname'] ?? ''),
|
||||
'dept_id' => isset($item['dept_id']) ? (int) $item['dept_id'] : null,
|
||||
'label' => $label ?: (string) $item['id'],
|
||||
];
|
||||
})->toArray();
|
||||
return $this->success($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 超管:按渠道树状展示全部管理员;非超管:同 getSystemUserOptions 扁平列表
|
||||
*/
|
||||
#[Permission('玩家列表', 'dice:player:index:index')]
|
||||
public function getSystemUserTreeOptions(Request $request): Response
|
||||
{
|
||||
if (!AdminScopeHelper::isSuperAdmin($this->adminInfo ?? null)) {
|
||||
return $this->getSystemUserOptions($request);
|
||||
}
|
||||
|
||||
$users = SystemUser::field('id,username,realname,dept_id')
|
||||
->where('status', 1)
|
||||
->order('id', 'asc')
|
||||
->select()
|
||||
->toArray();
|
||||
|
||||
$depts = SystemDept::field('id,name')
|
||||
->where('status', 1)
|
||||
->order('id', 'asc')
|
||||
->select()
|
||||
->toArray();
|
||||
|
||||
$deptNameMap = [];
|
||||
foreach ($depts as $dept) {
|
||||
$deptNameMap[(int) $dept['id']] = (string) ($dept['name'] ?? $dept['id']);
|
||||
}
|
||||
|
||||
$grouped = [];
|
||||
$unassigned = [];
|
||||
foreach ($users as $user) {
|
||||
$item = [
|
||||
'id' => (int) $user['id'],
|
||||
'username' => (string) ($user['username'] ?? ''),
|
||||
'realname' => (string) ($user['realname'] ?? ''),
|
||||
'dept_id' => isset($user['dept_id']) ? (int) $user['dept_id'] : null,
|
||||
];
|
||||
$label = trim($item['realname']) ?: $item['username'];
|
||||
$item['label'] = $label ?: (string) $item['id'];
|
||||
$deptId = $item['dept_id'] ?? 0;
|
||||
if ($deptId > 0 && isset($deptNameMap[$deptId])) {
|
||||
if (!isset($grouped[$deptId])) {
|
||||
$grouped[$deptId] = [];
|
||||
}
|
||||
$grouped[$deptId][] = $item;
|
||||
} else {
|
||||
$unassigned[] = $item;
|
||||
}
|
||||
}
|
||||
|
||||
$tree = [];
|
||||
foreach ($depts as $dept) {
|
||||
$deptId = (int) $dept['id'];
|
||||
$children = $grouped[$deptId] ?? [];
|
||||
if ($children === []) {
|
||||
continue;
|
||||
}
|
||||
$tree[] = [
|
||||
'id' => 'dept_' . $deptId,
|
||||
'label' => (string) ($dept['name'] ?? $deptId),
|
||||
'disabled' => true,
|
||||
'children' => $children,
|
||||
];
|
||||
}
|
||||
|
||||
if ($unassigned !== []) {
|
||||
$tree[] = [
|
||||
'id' => 'dept_unassigned',
|
||||
'label' => '__unassigned__',
|
||||
'disabled' => true,
|
||||
'children' => $unassigned,
|
||||
];
|
||||
}
|
||||
|
||||
return $this->success($tree);
|
||||
}
|
||||
|
||||
/**
|
||||
* 数据列表
|
||||
* @param Request $request
|
||||
@@ -92,9 +187,11 @@ class DicePlayerController extends BaseController
|
||||
['status', ''],
|
||||
['coin', ''],
|
||||
['lottery_config_id', ''],
|
||||
['create_time_min', ''],
|
||||
['create_time_max', ''],
|
||||
]);
|
||||
$query = $this->logic->search($where);
|
||||
AdminScopeHelper::applyAdminScope($query, $this->adminInfo ?? null);
|
||||
AdminScopeHelper::applyAdminScope($query, $this->adminInfo ?? null, $request->input('dept_id'));
|
||||
$query->with(['diceLotteryPoolConfig']);
|
||||
$data = $this->logic->getList($query);
|
||||
return $this->success($data);
|
||||
@@ -113,8 +210,7 @@ class DicePlayerController extends BaseController
|
||||
if (!$model) {
|
||||
return $this->fail('not found');
|
||||
}
|
||||
$allowedIds = AdminScopeHelper::getAllowedAdminIds($this->adminInfo ?? null);
|
||||
if ($allowedIds !== null && !in_array((int) ($model->admin_id ?? 0), $allowedIds, true)) {
|
||||
if (!AdminScopeHelper::canAccessDept($this->adminInfo ?? null, $model->dept_id ?? null)) {
|
||||
return $this->fail('no permission to view this record');
|
||||
}
|
||||
$data = is_array($model) ? $model : $model->toArray();
|
||||
@@ -130,20 +226,32 @@ class DicePlayerController extends BaseController
|
||||
public function save(Request $request): Response
|
||||
{
|
||||
$data = $request->post();
|
||||
$this->validate('save', $data);
|
||||
// 类型转化
|
||||
if (empty($data['admin_id']) && isset($this->adminInfo['id']) && (int) $this->adminInfo['id'] > 0) {
|
||||
$data['admin_id'] = (int) $this->adminInfo['id'];
|
||||
}
|
||||
AdminScopeHelper::prepareBusinessSaveData(
|
||||
$data,
|
||||
$this->adminInfo ?? null,
|
||||
$request->input('dept_id'),
|
||||
$data
|
||||
);
|
||||
$this->validate('save', $data);
|
||||
try {
|
||||
$result = $this->logic->add($data);
|
||||
if ($result && isset($result['id'])) {
|
||||
// 出于安全:删除该玩家缓存,后续 API 按需重建
|
||||
UserCache::deleteUser($result['id']);
|
||||
$player = DicePlayer::find($result['id']);
|
||||
} catch (\Throwable $e) {
|
||||
if (self::isDeptUsernameDuplicateException($e)) {
|
||||
return $this->fail('PLAYER_USERNAME_DEPT_UNIQUE');
|
||||
}
|
||||
throw $e;
|
||||
}
|
||||
$playerId = is_array($result) ? ($result['id'] ?? null) : $result;
|
||||
if ($playerId) {
|
||||
UserCache::deleteUser($playerId);
|
||||
$player = DicePlayer::find($playerId);
|
||||
if ($player && $player->username !== '') {
|
||||
UserCache::deletePlayerByUsername($player->username);
|
||||
}
|
||||
return $this->success('add success');
|
||||
return $this->success('ADD_SUCCESS');
|
||||
}
|
||||
return $this->fail('add failed');
|
||||
}
|
||||
@@ -157,14 +265,16 @@ class DicePlayerController extends BaseController
|
||||
public function update(Request $request): Response
|
||||
{
|
||||
$data = $request->post();
|
||||
$this->validate('update', $data);
|
||||
$model = $this->logic->read($data['id'] ?? 0);
|
||||
if ($model) {
|
||||
$allowedIds = AdminScopeHelper::getAllowedAdminIds($this->adminInfo ?? null);
|
||||
if ($allowedIds !== null && !in_array((int) ($model->admin_id ?? 0), $allowedIds, true)) {
|
||||
if (!AdminScopeHelper::canAccessDept($this->adminInfo ?? null, $model->dept_id ?? null, $request->input('dept_id'))) {
|
||||
return $this->fail('no permission to update this record');
|
||||
}
|
||||
if (!isset($data['dept_id']) || $data['dept_id'] === '' || $data['dept_id'] === null) {
|
||||
$data['dept_id'] = $model->dept_id ?? null;
|
||||
}
|
||||
}
|
||||
$this->validate('update', $data);
|
||||
$result = $this->logic->edit($data['id'], $data);
|
||||
if ($result) {
|
||||
// 出于安全:删除该玩家缓存,后续 API 按需重建
|
||||
@@ -173,7 +283,7 @@ class DicePlayerController extends BaseController
|
||||
if ($player && $player->username !== '') {
|
||||
UserCache::deletePlayerByUsername($player->username);
|
||||
}
|
||||
return $this->success('update success');
|
||||
return $this->success('UPDATE_SUCCESS');
|
||||
}
|
||||
return $this->fail('update failed');
|
||||
}
|
||||
@@ -196,8 +306,7 @@ class DicePlayerController extends BaseController
|
||||
}
|
||||
$model = $this->logic->read($id);
|
||||
if ($model) {
|
||||
$allowedIds = AdminScopeHelper::getAllowedAdminIds($this->adminInfo ?? null);
|
||||
if ($allowedIds !== null && !in_array((int) ($model->admin_id ?? 0), $allowedIds, true)) {
|
||||
if (!AdminScopeHelper::canAccessDept($this->adminInfo ?? null, $model->dept_id ?? null, $request->input('dept_id'))) {
|
||||
return $this->fail('no permission to update this record');
|
||||
}
|
||||
}
|
||||
@@ -227,8 +336,7 @@ class DicePlayerController extends BaseController
|
||||
if (!$player) {
|
||||
return $this->fail('not found');
|
||||
}
|
||||
$allowedIds = AdminScopeHelper::getAllowedAdminIds($this->adminInfo ?? null);
|
||||
if ($allowedIds !== null && !in_array((int) ($player->admin_id ?? 0), $allowedIds, true)) {
|
||||
if (!AdminScopeHelper::canAccessDept($this->adminInfo ?? null, $player->dept_id ?? null)) {
|
||||
return $this->fail('no permission to view this record');
|
||||
}
|
||||
if ((int) ($player->status ?? 1) === 0) {
|
||||
@@ -255,6 +363,7 @@ class DicePlayerController extends BaseController
|
||||
return $this->fail('generate token failed');
|
||||
}
|
||||
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();
|
||||
UserCache::setUser((int) $player->id, $userArr);
|
||||
UserCache::setPlayerByUsername($username, $userArr);
|
||||
@@ -263,6 +372,9 @@ class DicePlayerController extends BaseController
|
||||
if ($gameUrlBase === '') {
|
||||
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));
|
||||
$url = $gameUrlBase . '/?token=' . $tokenInUrl . '&lang=' . $lang;
|
||||
return $this->success(['url' => $url]);
|
||||
@@ -281,13 +393,12 @@ class DicePlayerController extends BaseController
|
||||
return $this->fail('please select data to delete');
|
||||
}
|
||||
$ids = is_array($ids) ? $ids : explode(',', (string) $ids);
|
||||
$allowedIds = AdminScopeHelper::getAllowedAdminIds($this->adminInfo ?? null);
|
||||
if ($allowedIds !== null) {
|
||||
$models = $this->logic->model->whereIn('id', $ids)->column('admin_id', 'id');
|
||||
$deptId = AdminScopeHelper::getDeptId($this->adminInfo ?? null);
|
||||
if ($deptId !== null) {
|
||||
$models = $this->logic->model->whereIn('id', $ids)->column('dept_id', 'id');
|
||||
$validIds = [];
|
||||
foreach ($ids as $id) {
|
||||
$adminId = (int) ($models[$id] ?? 0);
|
||||
if (in_array($adminId, $allowedIds, true)) {
|
||||
if (AdminScopeHelper::canAccessDept($this->adminInfo ?? null, $models[$id] ?? null)) {
|
||||
$validIds[] = $id;
|
||||
}
|
||||
}
|
||||
@@ -311,4 +422,17 @@ class DicePlayerController extends BaseController
|
||||
return $this->fail('delete failed');
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否违反 dice_player (dept_id, username) 唯一索引
|
||||
*/
|
||||
private static function isDeptUsernameDuplicateException(\Throwable $e): bool
|
||||
{
|
||||
$message = $e->getMessage();
|
||||
if ($message === '') {
|
||||
return false;
|
||||
}
|
||||
return str_contains($message, 'uk_dice_player_dept_username')
|
||||
|| (str_contains($message, 'Duplicate entry') && str_contains($message, 'username'));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -53,7 +53,7 @@ class DicePlayerTicketRecordController extends BaseController
|
||||
['create_time_max', ''],
|
||||
]);
|
||||
$query = $this->logic->search($where);
|
||||
AdminScopeHelper::applyAdminScope($query, $this->adminInfo ?? null);
|
||||
AdminScopeHelper::applyAdminScope($query, $this->adminInfo ?? null, $request->input('dept_id'));
|
||||
$query->with([
|
||||
'dicePlayer',
|
||||
]);
|
||||
@@ -70,7 +70,7 @@ class DicePlayerTicketRecordController extends BaseController
|
||||
public function getPlayerOptions(Request $request): Response
|
||||
{
|
||||
$query = DicePlayer::field('id,username');
|
||||
AdminScopeHelper::applyAdminScope($query, $this->adminInfo ?? null);
|
||||
AdminScopeHelper::applyAdminScope($query, $this->adminInfo ?? null, $request->input('dept_id'));
|
||||
$list = $query->select();
|
||||
$data = $list->map(function ($item) {
|
||||
return ['id' => $item['id'], 'username' => $item['username'] ?? ''];
|
||||
@@ -91,8 +91,7 @@ class DicePlayerTicketRecordController extends BaseController
|
||||
if (!$model) {
|
||||
return $this->fail('not found');
|
||||
}
|
||||
$allowedIds = AdminScopeHelper::getAllowedAdminIds($this->adminInfo ?? null);
|
||||
if ($allowedIds !== null && !in_array((int) ($model->admin_id ?? 0), $allowedIds, true)) {
|
||||
if (!AdminScopeHelper::canAccessDept($this->adminInfo ?? null, $model->dept_id ?? null, $request->input('dept_id'))) {
|
||||
return $this->fail('no permission to view this record');
|
||||
}
|
||||
$data = is_array($model) ? $model : $model->toArray();
|
||||
@@ -109,6 +108,7 @@ class DicePlayerTicketRecordController extends BaseController
|
||||
{
|
||||
$data = $request->post();
|
||||
$this->validate('save', $data);
|
||||
AdminScopeHelper::prepareBusinessSaveData($data, $this->adminInfo ?? null, $request->input('dept_id'), $data);
|
||||
$result = $this->logic->add($data);
|
||||
if ($result) {
|
||||
return $this->success('add success');
|
||||
@@ -117,24 +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);
|
||||
$result = $this->logic->edit($data['id'], $data);
|
||||
if ($result) {
|
||||
return $this->success('update success');
|
||||
} else {
|
||||
return $this->fail('update failed');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除数据
|
||||
* @param Request $request
|
||||
|
||||
@@ -47,13 +47,28 @@ class DicePlayerWalletRecordController extends BaseController
|
||||
['create_time_min', ''],
|
||||
['create_time_max', ''],
|
||||
]);
|
||||
$deptId = $request->input('dept_id');
|
||||
$query = $this->logic->search($where);
|
||||
AdminScopeHelper::applyAdminScope($query, $this->adminInfo ?? null);
|
||||
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([
|
||||
'dicePlayer',
|
||||
'operator',
|
||||
]);
|
||||
|
||||
$data = $this->logic->getList($query);
|
||||
$data['total_coin_change'] = $totalCoinChange;
|
||||
$data['total_coin_inflow'] = $totalCoinInflow;
|
||||
$data['total_coin_outflow'] = $totalCoinOutflow;
|
||||
return $this->success($data);
|
||||
}
|
||||
|
||||
@@ -66,7 +81,7 @@ class DicePlayerWalletRecordController extends BaseController
|
||||
public function getPlayerOptions(Request $request): Response
|
||||
{
|
||||
$query = DicePlayer::field('id,username');
|
||||
AdminScopeHelper::applyAdminScope($query, $this->adminInfo ?? null);
|
||||
AdminScopeHelper::applyAdminScope($query, $this->adminInfo ?? null, $request->input('dept_id'));
|
||||
$list = $query->select();
|
||||
$data = $list->map(function ($item) {
|
||||
return ['id' => $item['id'], 'username' => $item['username'] ?? ''];
|
||||
@@ -87,12 +102,11 @@ class DicePlayerWalletRecordController extends BaseController
|
||||
if ($playerId === null || $playerId === '') {
|
||||
return $this->fail('missing player_id');
|
||||
}
|
||||
$player = DicePlayer::field('coin,admin_id')->where('id', $playerId)->find();
|
||||
$player = DicePlayer::field('coin,dept_id')->where('id', $playerId)->find();
|
||||
if (!$player) {
|
||||
return $this->fail('Player not found');
|
||||
}
|
||||
$allowedIds = AdminScopeHelper::getAllowedAdminIds($this->adminInfo ?? null);
|
||||
if ($allowedIds !== null && !in_array((int) ($player->admin_id ?? 0), $allowedIds, true)) {
|
||||
if (!AdminScopeHelper::canAccessDept($this->adminInfo ?? null, $player->dept_id ?? null)) {
|
||||
return $this->fail('no permission to operate this player');
|
||||
}
|
||||
return $this->success(['wallet_before' => (float) $player['coin']]);
|
||||
@@ -111,8 +125,7 @@ class DicePlayerWalletRecordController extends BaseController
|
||||
if (!$model) {
|
||||
return $this->fail('not found');
|
||||
}
|
||||
$allowedIds = AdminScopeHelper::getAllowedAdminIds($this->adminInfo ?? null);
|
||||
if ($allowedIds !== null && !in_array((int) ($model->admin_id ?? 0), $allowedIds, true)) {
|
||||
if (!AdminScopeHelper::canAccessDept($this->adminInfo ?? null, $model->dept_id ?? null)) {
|
||||
return $this->fail('no permission to view this record');
|
||||
}
|
||||
$data = is_array($model) ? $model : $model->toArray();
|
||||
@@ -166,13 +179,10 @@ class DicePlayerWalletRecordController extends BaseController
|
||||
return $this->fail('please login first');
|
||||
}
|
||||
|
||||
$player = DicePlayer::field('admin_id')->where('id', $playerId)->find();
|
||||
if ($player) {
|
||||
$allowedIds = AdminScopeHelper::getAllowedAdminIds($this->adminInfo ?? null);
|
||||
if ($allowedIds !== null && !in_array((int) ($player->admin_id ?? 0), $allowedIds, true)) {
|
||||
$player = DicePlayer::field('dept_id')->where('id', $playerId)->find();
|
||||
if ($player && !AdminScopeHelper::canAccessDept($this->adminInfo ?? null, $player->dept_id ?? null, $request->input('dept_id'))) {
|
||||
return $this->fail('no permission to operate this player');
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
$this->logic->adminOperate($data, $adminId);
|
||||
@@ -183,20 +193,21 @@ class DicePlayerWalletRecordController extends BaseController
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新数据
|
||||
* 保存数据
|
||||
* @param Request $request
|
||||
* @return Response
|
||||
*/
|
||||
#[Permission('玩家钱包流水修改', 'dice:player_wallet_record:index:update')]
|
||||
public function update(Request $request): Response
|
||||
#[Permission('玩家钱包流水添加', 'dice:player_wallet_record:index:save')]
|
||||
public function save(Request $request): Response
|
||||
{
|
||||
$data = $request->post();
|
||||
$this->validate('update', $data);
|
||||
$result = $this->logic->edit($data['id'], $data);
|
||||
$this->validate('save', $data);
|
||||
AdminScopeHelper::prepareBusinessSaveData($data, $this->adminInfo ?? null, $request->input('dept_id'), $data);
|
||||
$result = $this->logic->add($data);
|
||||
if ($result) {
|
||||
return $this->success('update success');
|
||||
} else {
|
||||
return $this->fail('update failed');
|
||||
return $this->success('add success');
|
||||
}
|
||||
return $this->fail('add failed');
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
// +----------------------------------------------------------------------
|
||||
namespace app\dice\controller\reward;
|
||||
|
||||
use app\dice\helper\AdminScopeHelper;
|
||||
use app\dice\logic\reward\DiceRewardLogic;
|
||||
use app\dice\logic\reward_config_record\DiceRewardConfigRecordLogic;
|
||||
use app\dice\model\reward\DiceReward;
|
||||
@@ -42,11 +43,18 @@ class DiceRewardController extends BaseController
|
||||
$orderType = $request->input('orderType', 'asc');
|
||||
|
||||
$logic = new DiceRewardLogic();
|
||||
$data = $logic->getListWithConfig($direction, [
|
||||
$data = $logic->getListWithConfig(
|
||||
$direction,
|
||||
[
|
||||
'tier' => $tier,
|
||||
'orderField' => $orderField,
|
||||
'orderType' => $orderType,
|
||||
], $page, $limit);
|
||||
],
|
||||
$page,
|
||||
$limit,
|
||||
$this->adminInfo ?? null,
|
||||
$request->input('dept_id')
|
||||
);
|
||||
return $this->success($data);
|
||||
}
|
||||
|
||||
@@ -61,8 +69,10 @@ class DiceRewardController extends BaseController
|
||||
if (!in_array($direction, [DiceReward::DIRECTION_CLOCKWISE, DiceReward::DIRECTION_COUNTERCLOCKWISE], true)) {
|
||||
$direction = DiceReward::DIRECTION_CLOCKWISE;
|
||||
}
|
||||
$deptId = AdminScopeHelper::resolveConfigDeptId($this->adminInfo ?? null, $request->input('dept_id'));
|
||||
DiceReward::refreshCache($deptId);
|
||||
$logic = new DiceRewardLogic();
|
||||
$data = $logic->getListGroupedByTierForDirection($direction);
|
||||
$data = $logic->getListGroupedByTierForDirection($direction, $deptId);
|
||||
return $this->success($data);
|
||||
}
|
||||
|
||||
@@ -74,9 +84,10 @@ class DiceRewardController extends BaseController
|
||||
#[Permission('奖励对照列表', 'dice:reward:index:index')]
|
||||
public function weightRatioListWithDirection(Request $request): Response
|
||||
{
|
||||
DiceReward::refreshCache();
|
||||
$deptId = AdminScopeHelper::resolveConfigDeptId($this->adminInfo ?? null, $request->input('dept_id'));
|
||||
DiceReward::refreshCache($deptId);
|
||||
$logic = new DiceRewardLogic();
|
||||
$data = $logic->getListGroupedByTierWithDirection();
|
||||
$data = $logic->getListGroupedByTierWithDirection($deptId);
|
||||
return $this->success($data);
|
||||
}
|
||||
|
||||
@@ -85,7 +96,7 @@ class DiceRewardController extends BaseController
|
||||
* 参数:lottery_config_id 可选;paid_tier_weights / free_tier_weights 自定义档位;
|
||||
* paid_s_count, paid_n_count
|
||||
* 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')]
|
||||
public function startWeightTest(Request $request): Response
|
||||
@@ -103,11 +114,15 @@ class DiceRewardController extends BaseController
|
||||
'chain_free_mode' => $post['chain_free_mode'] ?? null,
|
||||
'kill_mode_enabled' => $post['kill_mode_enabled'] ?? null,
|
||||
'test_safety_line' => $post['test_safety_line'] ?? null,
|
||||
'dept_id' => $post['dept_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;
|
||||
$requestDeptId = AdminScopeHelper::pickRequestDeptId($request->input('dept_id'), $post);
|
||||
try {
|
||||
$logic = new DiceRewardConfigRecordLogic();
|
||||
$recordId = $logic->createWeightTestRecord($params, $adminId);
|
||||
$recordId = $logic->createWeightTestRecord($params, $adminId, $this->adminInfo ?? null, $requestDeptId);
|
||||
return $this->success(['record_id' => $recordId]);
|
||||
} catch (\plugin\saiadmin\exception\ApiException $e) {
|
||||
return $this->fail($e->getMessage());
|
||||
@@ -167,8 +182,9 @@ class DiceRewardController extends BaseController
|
||||
return $this->fail('parameter items must be an array');
|
||||
}
|
||||
try {
|
||||
$deptId = AdminScopeHelper::resolveConfigDeptId($this->adminInfo ?? null, $request->input('dept_id'));
|
||||
$logic = new DiceRewardLogic();
|
||||
$logic->batchUpdateWeights($items);
|
||||
$logic->batchUpdateWeights($items, $deptId);
|
||||
return $this->success('save success');
|
||||
} catch (\plugin\saiadmin\exception\ApiException $e) {
|
||||
return $this->fail($e->getMessage());
|
||||
@@ -191,8 +207,9 @@ class DiceRewardController extends BaseController
|
||||
return $this->fail('parameter items must be an array');
|
||||
}
|
||||
try {
|
||||
$deptId = AdminScopeHelper::resolveConfigDeptId($this->adminInfo ?? null, $request->input('dept_id'));
|
||||
$logic = new DiceRewardLogic();
|
||||
$logic->batchUpdateWeightsByDirection($direction, $items);
|
||||
$logic->batchUpdateWeightsByDirection($direction, $items, $deptId);
|
||||
return $this->success('save success');
|
||||
} catch (\plugin\saiadmin\exception\ApiException $e) {
|
||||
return $this->fail($e->getMessage());
|
||||
|
||||
@@ -6,9 +6,11 @@
|
||||
// +----------------------------------------------------------------------
|
||||
namespace app\dice\controller\reward_config;
|
||||
|
||||
use app\dice\helper\AdminScopeHelper;
|
||||
use plugin\saiadmin\basic\BaseController;
|
||||
use app\dice\logic\reward_config\DiceRewardConfigLogic;
|
||||
use app\dice\logic\reward\DiceRewardLogic;
|
||||
use app\dice\model\reward\DiceReward;
|
||||
use app\dice\validate\reward_config\DiceRewardConfigValidate;
|
||||
use plugin\saiadmin\service\Permission;
|
||||
use support\Request;
|
||||
@@ -46,7 +48,9 @@ class DiceRewardConfigController extends BaseController
|
||||
['tier', ''],
|
||||
]);
|
||||
$query = $this->logic->search($where);
|
||||
$data = $this->logic->getList($query);
|
||||
AdminScopeHelper::applyConfigScope($query, $this->adminInfo ?? null, $request->input('dept_id'));
|
||||
// 奖励索引 + 大奖权重共约 32 条,配置页需一次返回本渠道全部数据
|
||||
$data = $query->order('id', 'asc')->select()->toArray();
|
||||
return $this->success($data);
|
||||
}
|
||||
|
||||
@@ -78,6 +82,7 @@ class DiceRewardConfigController extends BaseController
|
||||
{
|
||||
$data = $request->post();
|
||||
$this->validate('save', $data);
|
||||
AdminScopeHelper::prepareConfigSaveData($data, $this->adminInfo ?? null, $request->input('dept_id'), $data);
|
||||
$result = $this->logic->add($data);
|
||||
if ($result) {
|
||||
return $this->success('add success');
|
||||
@@ -95,8 +100,19 @@ class DiceRewardConfigController extends BaseController
|
||||
public function update(Request $request): Response
|
||||
{
|
||||
$data = $request->post();
|
||||
if ($data === [] || $data === null) {
|
||||
$data = $request->all();
|
||||
}
|
||||
$this->validate('update', $data);
|
||||
$result = $this->logic->edit($data['id'], $data);
|
||||
$requestDeptId = AdminScopeHelper::pickRequestDeptId($request->input('dept_id'), is_array($data) ? $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, $requestDeptId)) {
|
||||
return $this->fail('no permission to update this record');
|
||||
}
|
||||
}
|
||||
$result = $this->logic->edit($data['id'], $data, $this->adminInfo ?? null, $requestDeptId);
|
||||
if ($result) {
|
||||
return $this->success('update success');
|
||||
} else {
|
||||
@@ -111,6 +127,25 @@ class DiceRewardConfigController extends BaseController
|
||||
*/
|
||||
#[Permission('修改奖励索引', 'dice:reward_config:index:batchUpdate')]
|
||||
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', []);
|
||||
if (! is_array($items)) {
|
||||
@@ -123,7 +158,9 @@ class DiceRewardConfigController extends BaseController
|
||||
foreach ($items as $item) {
|
||||
$this->validate('batch_update', array_merge($item, ['id' => $item['id']]));
|
||||
}
|
||||
$this->logic->batchUpdate($items);
|
||||
$requestDeptId = AdminScopeHelper::pickRequestDeptId($request->input('dept_id'), $request->post());
|
||||
$deptId = AdminScopeHelper::resolveConfigDeptId($this->adminInfo ?? null, $requestDeptId);
|
||||
$this->logic->batchUpdate($items, $deptId);
|
||||
return $this->success('save success');
|
||||
}
|
||||
|
||||
@@ -139,7 +176,7 @@ class DiceRewardConfigController extends BaseController
|
||||
if (empty($ids)) {
|
||||
return $this->fail('please select data to delete');
|
||||
}
|
||||
$result = $this->logic->destroy($ids);
|
||||
$result = $this->logic->destroy($ids, $this->adminInfo ?? null, $request->input('dept_id'));
|
||||
if ($result) {
|
||||
return $this->success('delete success');
|
||||
} else {
|
||||
@@ -155,8 +192,10 @@ class DiceRewardConfigController extends BaseController
|
||||
#[Permission('奖励配置列表', 'dice:reward_config:index:index')]
|
||||
public function weightRatioList(Request $request): Response
|
||||
{
|
||||
$deptId = AdminScopeHelper::resolveConfigDeptId($this->adminInfo ?? null, $request->input('dept_id'));
|
||||
DiceReward::refreshCache($deptId);
|
||||
$rewardLogic = new DiceRewardLogic();
|
||||
$data = $rewardLogic->getListGroupedByTierWithDirection();
|
||||
$data = $rewardLogic->getListGroupedByTierWithDirection($deptId);
|
||||
return $this->success($data);
|
||||
}
|
||||
|
||||
@@ -174,8 +213,9 @@ class DiceRewardConfigController extends BaseController
|
||||
return $this->fail('parameter items must be an array');
|
||||
}
|
||||
try {
|
||||
$deptId = AdminScopeHelper::resolveConfigDeptId($this->adminInfo ?? null, $request->input('dept_id'));
|
||||
$rewardLogic = new DiceRewardLogic();
|
||||
$rewardLogic->batchUpdateWeights($items);
|
||||
$rewardLogic->batchUpdateWeights($items, $deptId);
|
||||
return $this->success('save success');
|
||||
} catch (\plugin\saiadmin\exception\ApiException $e) {
|
||||
return $this->fail($e->getMessage());
|
||||
@@ -199,7 +239,8 @@ class DiceRewardConfigController extends BaseController
|
||||
if ($err !== null) {
|
||||
return $this->fail($err);
|
||||
}
|
||||
$this->logic->batchUpdateBigwinWeight($items);
|
||||
$deptId = AdminScopeHelper::resolveConfigDeptId($this->adminInfo ?? null, $request->input('dept_id'));
|
||||
$this->logic->batchUpdateBigwinWeight($items, $deptId);
|
||||
return $this->success('save success');
|
||||
}
|
||||
|
||||
@@ -214,13 +255,31 @@ class DiceRewardConfigController extends BaseController
|
||||
{
|
||||
try {
|
||||
$rewardLogic = new DiceRewardLogic();
|
||||
$result = $rewardLogic->createRewardReferenceFromConfig();
|
||||
$deptId = AdminScopeHelper::resolveConfigDeptId($this->adminInfo ?? null, $request->input('dept_id'));
|
||||
$result = $rewardLogic->createRewardReferenceFromConfig($deptId);
|
||||
return $this->success($result, 'create reward mapping success');
|
||||
} catch (\plugin\saiadmin\exception\ApiException $e) {
|
||||
return $this->fail($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建奖励对照(预览):不写入 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。
|
||||
* @param Request $request test_count: 100|500|1000, save_record: bool, lottery_config_id: int|null 奖池配置ID,用于设定 T1-T5 概率
|
||||
|
||||
@@ -8,6 +8,7 @@ namespace app\dice\controller\reward_config_record;
|
||||
|
||||
use app\dice\logic\reward_config_record\DiceRewardConfigRecordLogic;
|
||||
use app\dice\validate\reward_config_record\DiceRewardConfigRecordValidate;
|
||||
use app\dice\helper\AdminScopeHelper;
|
||||
use plugin\saiadmin\basic\BaseController;
|
||||
use plugin\saiadmin\app\model\system\SystemUser;
|
||||
use plugin\saiadmin\service\Permission;
|
||||
@@ -42,6 +43,7 @@ class DiceRewardConfigRecordController extends BaseController
|
||||
['ante', ''],
|
||||
]);
|
||||
$query = $this->logic->search($where);
|
||||
AdminScopeHelper::applyAdminScope($query, $this->adminInfo ?? null, $request->input('dept_id'));
|
||||
$data = $this->logic->getList($query);
|
||||
return $this->success($data);
|
||||
}
|
||||
@@ -96,6 +98,7 @@ class DiceRewardConfigRecordController extends BaseController
|
||||
{
|
||||
$data = $request->post();
|
||||
$this->validate('save', $data);
|
||||
AdminScopeHelper::prepareBusinessSaveData($data, $this->adminInfo ?? null, $request->input('dept_id'), $data);
|
||||
$result = $this->logic->add($data);
|
||||
if ($result) {
|
||||
return $this->success('add success');
|
||||
@@ -104,24 +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);
|
||||
$result = $this->logic->edit($data['id'], $data);
|
||||
if ($result) {
|
||||
return $this->success('update success');
|
||||
} else {
|
||||
return $this->fail('update failed');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除数据
|
||||
* @param Request $request
|
||||
|
||||
@@ -3,21 +3,75 @@ declare(strict_types=1);
|
||||
|
||||
namespace app\dice\helper;
|
||||
|
||||
use app\dice\model\player\DicePlayer;
|
||||
use plugin\saiadmin\app\model\system\SystemUser;
|
||||
use plugin\saiadmin\exception\ApiException;
|
||||
|
||||
/**
|
||||
* 管理员数据范围辅助类
|
||||
* 用于获取当前管理员及其部门下属管理员可访问的数据范围
|
||||
* 大富翁数据范围:按渠道 dept_id 隔离(关联 sa_system_dept)
|
||||
*/
|
||||
class AdminScopeHelper
|
||||
{
|
||||
/** 超管查看默认配置模板 */
|
||||
public const DEFAULT_TEMPLATE_DEPT = 0;
|
||||
|
||||
/**
|
||||
* 获取当前管理员可访问的 admin_id 列表
|
||||
* 超级管理员(id=1) 返回 null 表示不限制
|
||||
* 普通管理员返回其本人及部门下属管理员的 id 列表
|
||||
* 当前管理员所属渠道 ID;超级管理员(id=1) 返回 null 表示不限制
|
||||
*/
|
||||
public static function getDeptId(?array $adminInfo): ?int
|
||||
{
|
||||
if (empty($adminInfo) || !isset($adminInfo['id'])) {
|
||||
return null;
|
||||
}
|
||||
$adminId = (int) $adminInfo['id'];
|
||||
if ($adminId <= 1) {
|
||||
return null;
|
||||
}
|
||||
$deptList = $adminInfo['deptList'] ?? [];
|
||||
if (!empty($deptList['id'])) {
|
||||
return (int) $deptList['id'];
|
||||
}
|
||||
if (!empty($adminInfo['dept_id'])) {
|
||||
return (int) $adminInfo['dept_id'];
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
public static function isSuperAdmin(?array $adminInfo): bool
|
||||
{
|
||||
return !empty($adminInfo['id']) && (int) $adminInfo['id'] <= 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析配置类接口的渠道 ID(请求参数 dept_id)
|
||||
* 超管:0 或空=默认模板(null);>0=指定渠道
|
||||
* 普通管理员:固定本渠道
|
||||
*/
|
||||
public static function resolveConfigDeptId(?array $adminInfo, $requestDeptId): int
|
||||
{
|
||||
$scopeDeptId = self::getDeptId($adminInfo);
|
||||
if ($scopeDeptId !== null) {
|
||||
return $scopeDeptId > 0 ? $scopeDeptId : self::DEFAULT_TEMPLATE_DEPT;
|
||||
}
|
||||
if ($requestDeptId === null || $requestDeptId === '') {
|
||||
return self::DEFAULT_TEMPLATE_DEPT;
|
||||
}
|
||||
$id = (int) $requestDeptId;
|
||||
if ($id === self::DEFAULT_TEMPLATE_DEPT) {
|
||||
return self::DEFAULT_TEMPLATE_DEPT;
|
||||
}
|
||||
return $id > 0 ? $id : self::DEFAULT_TEMPLATE_DEPT;
|
||||
}
|
||||
|
||||
public static function isTemplateDeptId($deptId): bool
|
||||
{
|
||||
return $deptId === null || $deptId === '' || (int) $deptId === self::DEFAULT_TEMPLATE_DEPT;
|
||||
}
|
||||
|
||||
/**
|
||||
* 同渠道下可访问的管理员 ID
|
||||
*
|
||||
* @param array|null $adminInfo 当前登录管理员信息(含 id、deptList)
|
||||
* @return int[]|null null=不限制(超级管理员),否则为可访问的 admin_id 数组
|
||||
* @return int[]|null null=不限制
|
||||
*/
|
||||
public static function getAllowedAdminIds(?array $adminInfo): ?array
|
||||
{
|
||||
@@ -28,33 +82,285 @@ class AdminScopeHelper
|
||||
if ($adminId <= 1) {
|
||||
return null;
|
||||
}
|
||||
$deptList = $adminInfo['deptList'] ?? [];
|
||||
if (empty($deptList) || !isset($deptList['id'])) {
|
||||
$deptId = self::getDeptId($adminInfo);
|
||||
if ($deptId === null) {
|
||||
return null;
|
||||
}
|
||||
if ($deptId <= 0) {
|
||||
return [$adminId];
|
||||
}
|
||||
$query = SystemUser::field('id');
|
||||
$query->auth($deptList);
|
||||
$ids = $query->column('id');
|
||||
return array_map('intval', $ids ?: []);
|
||||
$ids = SystemUser::where('dept_id', $deptId)->column('id');
|
||||
return array_map('intval', $ids ?: [$adminId]);
|
||||
}
|
||||
|
||||
public static function fillDeptId(array &$data, ?array $adminInfo, $requestDeptId = null): void
|
||||
{
|
||||
if (isset($data['dept_id']) && $data['dept_id'] !== '' && $data['dept_id'] !== null) {
|
||||
return;
|
||||
}
|
||||
$deptId = self::resolveConfigDeptId($adminInfo, $requestDeptId ?? ($data['filter_dept_id'] ?? null));
|
||||
if ($deptId > 0) {
|
||||
$data['dept_id'] = $deptId;
|
||||
} elseif (!isset($data['dept_id']) || self::isTemplateDeptId($data['dept_id'])) {
|
||||
$data['dept_id'] = self::DEFAULT_TEMPLATE_DEPT;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 对查询应用 admin_id 范围过滤
|
||||
*
|
||||
* @param object $query ThinkORM 查询对象
|
||||
* @param array|null $adminInfo 当前登录管理员信息
|
||||
* @return void
|
||||
* 业务数据新增:按渠道写入 dept_id(玩家、记录等)
|
||||
* 优先级:已有 dept_id → 所属管理员 admin_id → 请求渠道 → 当前登录人渠道
|
||||
*/
|
||||
public static function applyAdminScope($query, ?array $adminInfo): void
|
||||
public static function fillBusinessDeptId(array &$data, ?array $adminInfo, $requestDeptId = null): void
|
||||
{
|
||||
$allowedIds = self::getAllowedAdminIds($adminInfo);
|
||||
if ($allowedIds === null) {
|
||||
if (isset($data['dept_id']) && $data['dept_id'] !== '' && $data['dept_id'] !== null) {
|
||||
$data['dept_id'] = (int) $data['dept_id'];
|
||||
if ($data['dept_id'] > 0) {
|
||||
return;
|
||||
}
|
||||
if (empty($allowedIds)) {
|
||||
}
|
||||
|
||||
if (!empty($data['player_id'])) {
|
||||
$playerDeptId = self::resolveDeptIdByPlayerId($data['player_id']);
|
||||
if ($playerDeptId !== null && $playerDeptId > 0) {
|
||||
$data['dept_id'] = $playerDeptId;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($data['admin_id'])) {
|
||||
$ownerDeptId = self::resolveDeptIdByAdminId($data['admin_id']);
|
||||
if ($ownerDeptId !== null && $ownerDeptId > 0) {
|
||||
$data['dept_id'] = $ownerDeptId;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
$deptId = self::resolveBusinessDeptId($adminInfo, $requestDeptId);
|
||||
if ($deptId !== null && $deptId > 0) {
|
||||
$data['dept_id'] = $deptId;
|
||||
return;
|
||||
}
|
||||
|
||||
$scopeDeptId = self::getDeptId($adminInfo);
|
||||
if ($scopeDeptId !== null && $scopeDeptId > 0) {
|
||||
$data['dept_id'] = $scopeDeptId;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 业务新增:解析请求渠道并填充 dept_id,缺失时抛错
|
||||
*/
|
||||
public static function prepareBusinessSaveData(
|
||||
array &$data,
|
||||
?array $adminInfo,
|
||||
$inputDeptId = null,
|
||||
array $body = []
|
||||
): void {
|
||||
$requestDeptId = self::pickRequestDeptId($inputDeptId, $body);
|
||||
self::fillBusinessDeptId($data, $adminInfo, $requestDeptId);
|
||||
self::assertBusinessDeptId($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 配置新增:解析请求渠道并填充 dept_id
|
||||
*/
|
||||
public static function prepareConfigSaveData(
|
||||
array &$data,
|
||||
?array $adminInfo,
|
||||
$inputDeptId = null,
|
||||
array $body = []
|
||||
): void {
|
||||
$requestDeptId = self::pickRequestDeptId($inputDeptId, $body);
|
||||
self::fillDeptId($data, $adminInfo, $requestDeptId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 业务表 dept_id 必须 > 0(非默认模板)
|
||||
*/
|
||||
public static function assertBusinessDeptId(array $data): void
|
||||
{
|
||||
if (!isset($data['dept_id']) || $data['dept_id'] === '' || $data['dept_id'] === null) {
|
||||
throw new ApiException('CHANNEL_DEPT_ID_REQUIRED');
|
||||
}
|
||||
if ((int) $data['dept_id'] <= 0) {
|
||||
throw new ApiException('INVALID_CHANNEL_DEPT_ID');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据玩家 ID 解析所属渠道
|
||||
*/
|
||||
public static function resolveDeptIdByPlayerId($playerId): ?int
|
||||
{
|
||||
if ($playerId === null || $playerId === '') {
|
||||
return null;
|
||||
}
|
||||
$player = DicePlayer::field('dept_id,admin_id')->find($playerId);
|
||||
if (!$player || $player->isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
$deptId = $player->dept_id ?? null;
|
||||
if ($deptId !== null && $deptId !== '' && (int) $deptId > 0) {
|
||||
return (int) $deptId;
|
||||
}
|
||||
if (!empty($player->admin_id)) {
|
||||
return self::resolveDeptIdByAdminId($player->admin_id);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据后台管理员 ID 解析所属渠道
|
||||
*/
|
||||
public static function resolveDeptIdByAdminId($adminId): ?int
|
||||
{
|
||||
if ($adminId === null || $adminId === '') {
|
||||
return null;
|
||||
}
|
||||
$admin = SystemUser::find($adminId);
|
||||
if (!$admin || $admin->isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
$deptId = $admin->dept_id ?? null;
|
||||
if ($deptId !== null && $deptId !== '' && (int) $deptId > 0) {
|
||||
return (int) $deptId;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 规范化记录上的 dept_id(null 视为默认模板 0)
|
||||
*/
|
||||
public static function normalizeRecordDeptId($recordDeptId): int
|
||||
{
|
||||
if ($recordDeptId === null || $recordDeptId === '') {
|
||||
return self::DEFAULT_TEMPLATE_DEPT;
|
||||
}
|
||||
return (int) $recordDeptId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 玩家端读取游戏配置时使用的渠道 ID(玩家 dept_id 优先,否则按所属管理员)
|
||||
*/
|
||||
public static function resolvePlayerConfigDeptId($player): int
|
||||
{
|
||||
$deptId = null;
|
||||
$adminId = null;
|
||||
if (is_array($player)) {
|
||||
$deptId = $player['dept_id'] ?? null;
|
||||
$adminId = $player['admin_id'] ?? null;
|
||||
} elseif (is_object($player)) {
|
||||
$deptId = $player->dept_id ?? null;
|
||||
$adminId = $player->admin_id ?? null;
|
||||
}
|
||||
if ($deptId !== null && $deptId !== '' && (int) $deptId > 0) {
|
||||
return (int) $deptId;
|
||||
}
|
||||
if ($adminId !== null && $adminId !== '') {
|
||||
$fromAdmin = self::resolveDeptIdByAdminId($adminId);
|
||||
if ($fromAdmin !== null && $fromAdmin > 0) {
|
||||
return $fromAdmin;
|
||||
}
|
||||
}
|
||||
return self::DEFAULT_TEMPLATE_DEPT;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从请求参数或请求体中解析 dept_id(兼容 PUT JSON 仅出现在 body 的情况)
|
||||
*/
|
||||
public static function pickRequestDeptId($inputDeptId, array $body = [])
|
||||
{
|
||||
if ($inputDeptId !== null && $inputDeptId !== '') {
|
||||
return $inputDeptId;
|
||||
}
|
||||
if (isset($body['dept_id']) && $body['dept_id'] !== '' && $body['dept_id'] !== null) {
|
||||
return $body['dept_id'];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static function canAccessDept(?array $adminInfo, $recordDeptId, $requestDeptId = null): bool
|
||||
{
|
||||
$recordDeptId = self::normalizeRecordDeptId($recordDeptId);
|
||||
$scopeDeptId = self::getDeptId($adminInfo);
|
||||
if ($scopeDeptId === null) {
|
||||
if ($requestDeptId === null || $requestDeptId === '') {
|
||||
return true;
|
||||
}
|
||||
$target = self::resolveConfigDeptId($adminInfo, $requestDeptId);
|
||||
if (self::isTemplateDeptId($target)) {
|
||||
return self::isTemplateDeptId($recordDeptId);
|
||||
}
|
||||
return $recordDeptId === $target;
|
||||
}
|
||||
if ($recordDeptId === self::DEFAULT_TEMPLATE_DEPT && $scopeDeptId > 0) {
|
||||
return false;
|
||||
}
|
||||
return $recordDeptId === $scopeDeptId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 业务页渠道 ID:超管通过请求 dept_id 筛选;未传或 <=0 时不限制
|
||||
*/
|
||||
public static function resolveBusinessDeptId(?array $adminInfo, $requestDeptId): ?int
|
||||
{
|
||||
$scopeDeptId = self::getDeptId($adminInfo);
|
||||
if ($scopeDeptId !== null) {
|
||||
return $scopeDeptId > 0 ? $scopeDeptId : null;
|
||||
}
|
||||
if ($requestDeptId === null || $requestDeptId === '') {
|
||||
return null;
|
||||
}
|
||||
$id = (int) $requestDeptId;
|
||||
return $id > 0 ? $id : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 业务数据列表(玩家、记录、工作台等)
|
||||
*/
|
||||
public static function applyAdminScope($query, ?array $adminInfo, $requestDeptId = null): void
|
||||
{
|
||||
if (self::getDeptId($adminInfo) === null) {
|
||||
$target = self::resolveBusinessDeptId($adminInfo, $requestDeptId);
|
||||
if ($target !== null && $target > 0) {
|
||||
$query->where('dept_id', $target);
|
||||
}
|
||||
return;
|
||||
}
|
||||
$deptId = self::getDeptId($adminInfo);
|
||||
if ($deptId <= 0) {
|
||||
$query->whereRaw('1=0');
|
||||
return;
|
||||
}
|
||||
$query->whereIn('admin_id', $allowedIds);
|
||||
$query->where('dept_id', $deptId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 配置类列表:超管按所选渠道/默认模板筛选
|
||||
*/
|
||||
public static function applyConfigScope($query, ?array $adminInfo, $requestDeptId = null, string $deptColumn = 'dept_id'): void
|
||||
{
|
||||
$targetDeptId = self::resolveConfigDeptId($adminInfo, $requestDeptId);
|
||||
$scopeDeptId = self::getDeptId($adminInfo);
|
||||
|
||||
if ($scopeDeptId !== null && !self::isSuperAdmin($adminInfo)) {
|
||||
if ($scopeDeptId <= 0) {
|
||||
$query->whereRaw('1=0');
|
||||
return;
|
||||
}
|
||||
$query->where($deptColumn, $scopeDeptId);
|
||||
return;
|
||||
}
|
||||
|
||||
if (self::isTemplateDeptId($targetDeptId)) {
|
||||
$templateId = self::DEFAULT_TEMPLATE_DEPT;
|
||||
$query->where(function ($q) use ($templateId, $deptColumn) {
|
||||
$q->where($deptColumn, $templateId)->whereOr($deptColumn, null);
|
||||
});
|
||||
return;
|
||||
}
|
||||
$query->where($deptColumn, $targetDeptId);
|
||||
}
|
||||
}
|
||||
|
||||
139
server/app/dice/helper/ConfigScopeEditHelper.php
Normal file
@@ -0,0 +1,139 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\dice\helper;
|
||||
|
||||
use plugin\saiadmin\exception\ApiException;
|
||||
|
||||
/**
|
||||
* 配置类数据按渠道隔离的更新(防止 find(id) 误更新其他渠道同业务主键行)
|
||||
*/
|
||||
class ConfigScopeEditHelper
|
||||
{
|
||||
/**
|
||||
* 在查询上附加渠道条件
|
||||
*/
|
||||
public static function applyDeptIdWhere($query, int $deptId, string $column = 'dept_id'): void
|
||||
{
|
||||
if (AdminScopeHelper::isTemplateDeptId($deptId)) {
|
||||
$query->where(function ($q) use ($deptId, $column) {
|
||||
$q->where($column, $deptId);
|
||||
if (method_exists($q, 'orWhereNull')) {
|
||||
$q->orWhereNull($column);
|
||||
} else {
|
||||
$q->whereOr($column, null);
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
$query->where($column, $deptId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 按主键 + 渠道更新配置行
|
||||
*
|
||||
* @param Model $model 模型实例(用于取表名、主键)
|
||||
* @param mixed $primaryKeyValue 列表/表单中的主键值
|
||||
* @param int $deptId 渠道 ID(0=默认模板)
|
||||
* @param array $data 更新字段
|
||||
* @param array $forbidden 禁止写入的字段名
|
||||
*/
|
||||
/**
|
||||
* 按主键更新(主键全局唯一表如 dice_lottery_pool_config)
|
||||
* 以库中记录的 dept_id 为准,避免请求未带 dept_id 时误按默认模板 0 查找失败
|
||||
*/
|
||||
public static function updateByPkAndDept(
|
||||
object $model,
|
||||
$primaryKeyValue,
|
||||
int $requestDeptId,
|
||||
array $data,
|
||||
array $forbidden = ['id', 'dept_id', 'create_time', 'update_time', 'delete_time', 'row_id'],
|
||||
?array $adminInfo = null,
|
||||
$rawRequestDeptId = null
|
||||
): bool {
|
||||
foreach ($forbidden as $field) {
|
||||
unset($data[$field]);
|
||||
}
|
||||
if ($data === []) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$pk = self::resolvePk($model);
|
||||
$record = $model->where($pk, $primaryKeyValue)->find();
|
||||
if ($record === null) {
|
||||
throw new ApiException('data not found');
|
||||
}
|
||||
|
||||
$recordDeptId = AdminScopeHelper::normalizeRecordDeptId(
|
||||
is_array($record) ? ($record['dept_id'] ?? null) : ($record->dept_id ?? null)
|
||||
);
|
||||
|
||||
if ($adminInfo !== null && ! AdminScopeHelper::canAccessDept($adminInfo, $recordDeptId, $rawRequestDeptId)) {
|
||||
throw new ApiException('no permission to update this record');
|
||||
}
|
||||
|
||||
if ($rawRequestDeptId !== null && $rawRequestDeptId !== '') {
|
||||
$targetDeptId = AdminScopeHelper::resolveConfigDeptId($adminInfo, $rawRequestDeptId);
|
||||
if ($targetDeptId !== $recordDeptId) {
|
||||
throw new ApiException('record does not belong to selected channel');
|
||||
}
|
||||
}
|
||||
|
||||
$query = $model->where($pk, $primaryKeyValue);
|
||||
self::applyDeptIdWhere($query, $recordDeptId);
|
||||
|
||||
$affected = $query->update($data);
|
||||
return $affected !== false;
|
||||
}
|
||||
|
||||
/**
|
||||
* dice_reward_config / dice_config:业务 id(0~25 等)+ 渠道更新
|
||||
*/
|
||||
public static function updateByBusinessIdAndDept(
|
||||
object $model,
|
||||
int $businessId,
|
||||
int $deptId,
|
||||
array $data,
|
||||
array $forbidden = ['id', 'dept_id', 'create_time', 'update_time', 'delete_time', 'row_id']
|
||||
): bool {
|
||||
foreach ($forbidden as $field) {
|
||||
unset($data[$field]);
|
||||
}
|
||||
if ($data === []) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$query = $model->where('id', $businessId);
|
||||
self::applyDeptIdWhere($query, $deptId);
|
||||
|
||||
$record = (clone $query)->find();
|
||||
if ($record === null) {
|
||||
throw new ApiException('config id=' . $businessId . ' not found for current channel');
|
||||
}
|
||||
|
||||
$affected = $query->update($data);
|
||||
return $affected !== false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 列表/读取:按主键 + 渠道取单条(避免 find(pk) 命中其他渠道)
|
||||
*/
|
||||
private static function resolvePk(object $model): string
|
||||
{
|
||||
if (method_exists($model, 'getPk')) {
|
||||
return (string) $model->getPk();
|
||||
}
|
||||
if (method_exists($model, 'getKeyName')) {
|
||||
return (string) $model->getKeyName();
|
||||
}
|
||||
return 'id';
|
||||
}
|
||||
|
||||
public static function findByPkAndDept(object $model, $primaryKeyValue, int $deptId)
|
||||
{
|
||||
$pk = self::resolvePk($model);
|
||||
$query = $model->where($pk, $primaryKeyValue);
|
||||
self::applyDeptIdWhere($query, $deptId);
|
||||
return $query->find();
|
||||
}
|
||||
}
|
||||
@@ -6,13 +6,15 @@
|
||||
// +----------------------------------------------------------------------
|
||||
namespace app\dice\logic\ante_config;
|
||||
|
||||
use app\dice\helper\AdminScopeHelper;
|
||||
use app\dice\helper\ConfigScopeEditHelper;
|
||||
use app\dice\model\ante_config\DiceAnteConfig;
|
||||
use plugin\saiadmin\basic\think\BaseLogic;
|
||||
use app\dice\basic\DiceBaseLogic;
|
||||
|
||||
/**
|
||||
* 底注配置逻辑层
|
||||
*/
|
||||
class DiceAnteConfigLogic extends BaseLogic
|
||||
class DiceAnteConfigLogic extends DiceBaseLogic
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
@@ -22,22 +24,35 @@ class DiceAnteConfigLogic extends BaseLogic
|
||||
public function add(array $data): mixed
|
||||
{
|
||||
return $this->transaction(function () use ($data) {
|
||||
$this->applyNameTitleFromMult($data);
|
||||
$this->normalizeDefaultField($data);
|
||||
$deptId = AdminScopeHelper::resolveConfigDeptId(null, $data['dept_id'] ?? AdminScopeHelper::DEFAULT_TEMPLATE_DEPT);
|
||||
if ((int) ($data['is_default'] ?? 0) === 1) {
|
||||
$this->clearOtherDefaults();
|
||||
$this->clearOtherDefaults(null, $deptId);
|
||||
}
|
||||
return parent::add($data);
|
||||
});
|
||||
}
|
||||
|
||||
public function edit($id, array $data): mixed
|
||||
public function edit($id, array $data, ?array $adminInfo = null, $requestDeptId = null): mixed
|
||||
{
|
||||
return $this->transaction(function () use ($id, $data) {
|
||||
$pickedDeptId = AdminScopeHelper::pickRequestDeptId($requestDeptId, $data);
|
||||
$deptId = AdminScopeHelper::resolveConfigDeptId($adminInfo, $pickedDeptId);
|
||||
return $this->transaction(function () use ($id, $data, $deptId, $adminInfo, $pickedDeptId) {
|
||||
$this->applyNameTitleFromMult($data);
|
||||
$this->normalizeDefaultField($data);
|
||||
if ((int) ($data['is_default'] ?? 0) === 1) {
|
||||
$this->clearOtherDefaults((int) $id);
|
||||
$this->clearOtherDefaults((int) $id, $deptId);
|
||||
}
|
||||
return parent::edit($id, $data);
|
||||
return ConfigScopeEditHelper::updateByPkAndDept(
|
||||
$this->model,
|
||||
$id,
|
||||
$deptId,
|
||||
$data,
|
||||
['id', 'dept_id', 'create_time', 'update_time', 'delete_time', 'row_id'],
|
||||
$adminInfo,
|
||||
$pickedDeptId
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -79,9 +94,25 @@ class DiceAnteConfigLogic extends BaseLogic
|
||||
$data['is_default'] = ((int) $data['is_default']) === 1 ? 1 : 0;
|
||||
}
|
||||
|
||||
private function clearOtherDefaults(?int $excludeId = null): void
|
||||
/** 名称、标题随底注倍率自动设为 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
|
||||
{
|
||||
$query = $this->model->where('is_default', 1);
|
||||
ConfigScopeEditHelper::applyDeptIdWhere($query, $deptId);
|
||||
if ($excludeId !== null && $excludeId > 0) {
|
||||
$query->where('id', '<>', $excludeId);
|
||||
}
|
||||
|
||||
@@ -6,7 +6,9 @@
|
||||
// +----------------------------------------------------------------------
|
||||
namespace app\dice\logic\config;
|
||||
|
||||
use plugin\saiadmin\basic\eloquent\BaseLogic;
|
||||
use app\dice\basic\DiceBaseLogic;
|
||||
use app\dice\helper\AdminScopeHelper;
|
||||
use app\dice\helper\ConfigScopeEditHelper;
|
||||
use plugin\saiadmin\exception\ApiException;
|
||||
use plugin\saiadmin\utils\Helper;
|
||||
use app\dice\model\config\DiceConfig;
|
||||
@@ -14,7 +16,7 @@ use app\dice\model\config\DiceConfig;
|
||||
/**
|
||||
* 摇色子配置逻辑层
|
||||
*/
|
||||
class DiceConfigLogic extends BaseLogic
|
||||
class DiceConfigLogic extends DiceBaseLogic
|
||||
{
|
||||
/**
|
||||
* 构造函数
|
||||
@@ -24,4 +26,13 @@ class DiceConfigLogic extends BaseLogic
|
||||
$this->model = new DiceConfig();
|
||||
}
|
||||
|
||||
public function edit($id, array $data, ?array $adminInfo = null, $requestDeptId = null): mixed
|
||||
{
|
||||
$deptId = AdminScopeHelper::resolveConfigDeptId(
|
||||
$adminInfo,
|
||||
AdminScopeHelper::pickRequestDeptId($requestDeptId, $data)
|
||||
);
|
||||
return ConfigScopeEditHelper::updateByBusinessIdAndDept($this->model, (int) $id, $deptId, $data);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -4,16 +4,33 @@
|
||||
// +----------------------------------------------------------------------
|
||||
namespace app\dice\logic\game;
|
||||
|
||||
use plugin\saiadmin\basic\eloquent\BaseLogic;
|
||||
use app\dice\basic\DiceBaseLogic;
|
||||
use app\dice\helper\AdminScopeHelper;
|
||||
use app\dice\helper\ConfigScopeEditHelper;
|
||||
use app\dice\model\game\DiceGame;
|
||||
|
||||
/**
|
||||
* 游戏管理逻辑层
|
||||
*/
|
||||
class DiceGameLogic extends BaseLogic
|
||||
class DiceGameLogic extends DiceBaseLogic
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$this->model = new DiceGame();
|
||||
}
|
||||
|
||||
public function edit($id, array $data, ?array $adminInfo = null, $requestDeptId = null): mixed
|
||||
{
|
||||
$pickedDeptId = AdminScopeHelper::pickRequestDeptId($requestDeptId, $data);
|
||||
$deptId = AdminScopeHelper::resolveConfigDeptId($adminInfo, $pickedDeptId);
|
||||
return ConfigScopeEditHelper::updateByPkAndDept(
|
||||
$this->model,
|
||||
$id,
|
||||
$deptId,
|
||||
$data,
|
||||
['id', 'dept_id', 'create_time', 'update_time', 'delete_time', 'row_id'],
|
||||
$adminInfo,
|
||||
$pickedDeptId
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,16 +6,22 @@
|
||||
// +----------------------------------------------------------------------
|
||||
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\ConfigScopeEditHelper;
|
||||
use app\dice\model\lottery_pool_config\DiceLotteryPoolConfig;
|
||||
use plugin\saiadmin\basic\think\BaseLogic;
|
||||
use app\dice\model\player\DicePlayer;
|
||||
use app\dice\basic\DiceBaseLogic;
|
||||
use plugin\saiadmin\exception\ApiException;
|
||||
use plugin\saiadmin\utils\Helper;
|
||||
use support\think\Cache;
|
||||
use support\think\Db;
|
||||
|
||||
/**
|
||||
* 色子奖池配置逻辑层
|
||||
*/
|
||||
class DiceLotteryPoolConfigLogic extends BaseLogic
|
||||
class DiceLotteryPoolConfigLogic extends DiceBaseLogic
|
||||
{
|
||||
/** Redis 当前彩金池(type=0 实例)key,无则按 type=0 创建 */
|
||||
private const REDIS_KEY_CURRENT_POOL = 'api:game:lottery_pool:default';
|
||||
@@ -30,19 +36,155 @@ class DiceLotteryPoolConfigLogic extends BaseLogic
|
||||
$this->model = new DiceLotteryPoolConfig();
|
||||
}
|
||||
|
||||
/**
|
||||
* 按渠道隔离更新(主键 id 全局唯一,仍校验 dept_id 防止越权)
|
||||
*/
|
||||
public function edit($id, array $data, ?array $adminInfo = null, $requestDeptId = null): mixed
|
||||
{
|
||||
$pickedDeptId = AdminScopeHelper::pickRequestDeptId($requestDeptId, $data);
|
||||
$deptId = AdminScopeHelper::resolveConfigDeptId($adminInfo, $pickedDeptId);
|
||||
$result = ConfigScopeEditHelper::updateByPkAndDept(
|
||||
$this->model,
|
||||
$id,
|
||||
$deptId,
|
||||
$data,
|
||||
['id', 'dept_id', 'create_time', 'update_time', 'delete_time', 'row_id'],
|
||||
$adminInfo,
|
||||
$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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前彩金池(type=0)+ 杀分权重为 type=1 的只读展示
|
||||
* profit_amount 每次从 DB 实时读取;t1_weight~t5_weight 来自 type=1(杀分权重,不可在弹窗内修改)
|
||||
*
|
||||
* @return array{id:int,name:string,safety_line:int,kill_enabled:int,t1_weight:int,...,t5_weight:int,profit_amount:float}
|
||||
*/
|
||||
public function getCurrentPool(): array
|
||||
public function getCurrentPool(int $deptId = AdminScopeHelper::DEFAULT_TEMPLATE_DEPT): array
|
||||
{
|
||||
$configType0 = DiceLotteryPoolConfig::where('name', 'default')->find();
|
||||
$query0 = DiceLotteryPoolConfig::where('name', 'default');
|
||||
if (AdminScopeHelper::isTemplateDeptId($deptId)) {
|
||||
$query0->where(function ($q) {
|
||||
$q->where('dept_id', AdminScopeHelper::DEFAULT_TEMPLATE_DEPT)
|
||||
->whereOr('dept_id', null);
|
||||
});
|
||||
} else {
|
||||
$query0->where('dept_id', $deptId);
|
||||
}
|
||||
$configType0 = $query0->find();
|
||||
if (!$configType0) {
|
||||
throw new ApiException('No name=default pool config found, please create one first');
|
||||
}
|
||||
$configType1 = DiceLotteryPoolConfig::where('name', 'killScore')->find();
|
||||
$query1 = DiceLotteryPoolConfig::where('name', 'killScore');
|
||||
if (AdminScopeHelper::isTemplateDeptId($deptId)) {
|
||||
$query1->where(function ($q) {
|
||||
$q->where('dept_id', AdminScopeHelper::DEFAULT_TEMPLATE_DEPT)
|
||||
->whereOr('dept_id', null);
|
||||
});
|
||||
} else {
|
||||
$query1->where('dept_id', $deptId);
|
||||
}
|
||||
$configType1 = $query1->find();
|
||||
$row0 = $configType0->toArray();
|
||||
$profitAmount = isset($row0['profit_amount']) ? (float) $row0['profit_amount'] : (isset($row0['ev']) ? (float) $row0['ev'] : 0.0);
|
||||
$pool = [
|
||||
@@ -66,9 +208,9 @@ class DiceLotteryPoolConfigLogic extends BaseLogic
|
||||
*
|
||||
* @param array{safety_line?:int,kill_enabled?:int} $data
|
||||
*/
|
||||
public function updateCurrentPool(array $data): void
|
||||
public function updateCurrentPool(array $data, int $deptId = AdminScopeHelper::DEFAULT_TEMPLATE_DEPT): void
|
||||
{
|
||||
$pool = $this->getCurrentPool();
|
||||
$pool = $this->getCurrentPool($deptId);
|
||||
$id = (int) $pool['id'];
|
||||
if (!array_key_exists('safety_line', $data) && !array_key_exists('kill_enabled', $data)) {
|
||||
return;
|
||||
@@ -83,17 +225,21 @@ class DiceLotteryPoolConfigLogic extends BaseLogic
|
||||
if ($update === []) {
|
||||
return;
|
||||
}
|
||||
DiceLotteryPoolConfig::where('id', $id)->update($update);
|
||||
$query = DiceLotteryPoolConfig::where('id', $id);
|
||||
ConfigScopeEditHelper::applyDeptIdWhere($query, $deptId);
|
||||
$query->update($update);
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置当前彩金池的玩家累计盈利:将 profit_amount 置为 0,并刷新 Redis 缓存
|
||||
*/
|
||||
public function resetProfitAmount(): void
|
||||
public function resetProfitAmount(int $deptId = AdminScopeHelper::DEFAULT_TEMPLATE_DEPT): void
|
||||
{
|
||||
$pool = $this->getCurrentPool();
|
||||
$pool = $this->getCurrentPool($deptId);
|
||||
$id = (int) $pool['id'];
|
||||
DiceLotteryPoolConfig::where('id', $id)->update(['profit_amount' => 0]);
|
||||
$query = DiceLotteryPoolConfig::where('id', $id);
|
||||
ConfigScopeEditHelper::applyDeptIdWhere($query, $deptId);
|
||||
$query->update(['profit_amount' => 0]);
|
||||
$pool['profit_amount'] = 0.0;
|
||||
Cache::set(self::REDIS_KEY_CURRENT_POOL, json_encode($pool), self::EXPIRE);
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
// +----------------------------------------------------------------------
|
||||
namespace app\dice\logic\play_record;
|
||||
|
||||
use plugin\saiadmin\basic\think\BaseLogic;
|
||||
use app\dice\basic\DiceBaseLogic;
|
||||
use plugin\saiadmin\exception\ApiException;
|
||||
use plugin\saiadmin\utils\Helper;
|
||||
use app\dice\model\play_record\DicePlayRecord;
|
||||
@@ -14,7 +14,7 @@ use app\dice\model\play_record\DicePlayRecord;
|
||||
/**
|
||||
* 玩家抽奖记录逻辑层
|
||||
*/
|
||||
class DicePlayRecordLogic extends BaseLogic
|
||||
class DicePlayRecordLogic extends DiceBaseLogic
|
||||
{
|
||||
/**
|
||||
* 构造函数
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
// +----------------------------------------------------------------------
|
||||
namespace app\dice\logic\play_record_test;
|
||||
|
||||
use plugin\saiadmin\basic\think\BaseLogic;
|
||||
use app\dice\basic\DiceBaseLogic;
|
||||
use plugin\saiadmin\exception\ApiException;
|
||||
use plugin\saiadmin\utils\Helper;
|
||||
use app\dice\model\play_record_test\DicePlayRecordTest;
|
||||
@@ -14,7 +14,7 @@ use app\dice\model\play_record_test\DicePlayRecordTest;
|
||||
/**
|
||||
* 玩家抽奖记录(测试数据)逻辑层
|
||||
*/
|
||||
class DicePlayRecordTestLogic extends BaseLogic
|
||||
class DicePlayRecordTestLogic extends DiceBaseLogic
|
||||
{
|
||||
/**
|
||||
* 构造函数
|
||||
|
||||
@@ -6,7 +6,8 @@
|
||||
// +----------------------------------------------------------------------
|
||||
namespace app\dice\logic\player;
|
||||
|
||||
use plugin\saiadmin\basic\think\BaseLogic;
|
||||
use app\dice\basic\DiceBaseLogic;
|
||||
use app\dice\model\lottery_pool_config\DiceLotteryPoolConfig;
|
||||
use plugin\saiadmin\exception\ApiException;
|
||||
use plugin\saiadmin\utils\Helper;
|
||||
use app\dice\model\player\DicePlayer;
|
||||
@@ -14,7 +15,7 @@ use app\dice\model\player\DicePlayer;
|
||||
/**
|
||||
* 大富翁-玩家逻辑层
|
||||
*/
|
||||
class DicePlayerLogic extends BaseLogic
|
||||
class DicePlayerLogic extends DiceBaseLogic
|
||||
{
|
||||
/** 密码加密盐(可与 config 统一) */
|
||||
private const PASSWORD_SALT = 'dice_player_salt_2024';
|
||||
@@ -48,9 +49,30 @@ class DicePlayerLogic extends BaseLogic
|
||||
} else {
|
||||
unset($data['password']);
|
||||
}
|
||||
$data = $this->applyLotteryPoolWeightsToPlayerData($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)
|
||||
*/
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
// +----------------------------------------------------------------------
|
||||
namespace app\dice\logic\player_ticket_record;
|
||||
|
||||
use plugin\saiadmin\basic\think\BaseLogic;
|
||||
use app\dice\basic\DiceBaseLogic;
|
||||
use plugin\saiadmin\exception\ApiException;
|
||||
use plugin\saiadmin\utils\Helper;
|
||||
use app\dice\model\player_ticket_record\DicePlayerTicketRecord;
|
||||
@@ -14,7 +14,7 @@ use app\dice\model\player_ticket_record\DicePlayerTicketRecord;
|
||||
/**
|
||||
* 抽奖券获取记录逻辑层
|
||||
*/
|
||||
class DicePlayerTicketRecordLogic extends BaseLogic
|
||||
class DicePlayerTicketRecordLogic extends DiceBaseLogic
|
||||
{
|
||||
/**
|
||||
* 构造函数
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
// +----------------------------------------------------------------------
|
||||
namespace app\dice\logic\player_wallet_record;
|
||||
|
||||
use plugin\saiadmin\basic\think\BaseLogic;
|
||||
use app\dice\basic\DiceBaseLogic;
|
||||
use plugin\saiadmin\exception\ApiException;
|
||||
use app\dice\model\player_wallet_record\DicePlayerWalletRecord;
|
||||
use app\dice\model\player\DicePlayer;
|
||||
@@ -15,7 +15,7 @@ use app\api\cache\UserCache;
|
||||
/**
|
||||
* 玩家钱包流水逻辑层
|
||||
*/
|
||||
class DicePlayerWalletRecordLogic extends BaseLogic
|
||||
class DicePlayerWalletRecordLogic extends DiceBaseLogic
|
||||
{
|
||||
/**
|
||||
* 构造函数
|
||||
@@ -83,9 +83,11 @@ class DicePlayerWalletRecordLogic extends BaseLogic
|
||||
}
|
||||
|
||||
$playerAdminId = ($player->admin_id ?? null) ? (int) $player->admin_id : null;
|
||||
$playerDeptId = ($player->dept_id ?? null) ? (int) $player->dept_id : null;
|
||||
$record = [
|
||||
'player_id' => $playerId,
|
||||
'admin_id' => $playerAdminId,
|
||||
'dept_id' => $playerDeptId,
|
||||
'coin' => $type === 3 ? $coin : -$coin,
|
||||
'type' => $type,
|
||||
'wallet_before' => $walletBefore,
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
// +----------------------------------------------------------------------
|
||||
namespace app\dice\logic\reward;
|
||||
|
||||
use app\dice\helper\AdminScopeHelper;
|
||||
use app\dice\helper\ConfigScopeEditHelper;
|
||||
use app\dice\model\reward\DiceReward;
|
||||
use app\dice\model\reward_config\DiceRewardConfig;
|
||||
use plugin\saiadmin\exception\ApiException;
|
||||
@@ -18,6 +20,16 @@ class DiceRewardLogic
|
||||
private const WEIGHT_MIN = 1;
|
||||
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'];
|
||||
|
||||
@@ -29,21 +41,33 @@ class DiceRewardLogic
|
||||
* @param int $limit
|
||||
* @return array{total: int, per_page: int, current_page: int, data: array}
|
||||
*/
|
||||
public function getListWithConfig(int $direction, array $where, int $page = 1, int $limit = 10): array
|
||||
{
|
||||
public function getListWithConfig(
|
||||
int $direction,
|
||||
array $where,
|
||||
int $page = 1,
|
||||
int $limit = 10,
|
||||
?array $adminInfo = null,
|
||||
$requestDeptId = null
|
||||
): array {
|
||||
$tier = isset($where['tier']) ? trim((string) $where['tier']) : '';
|
||||
$orderField = isset($where['orderField']) && $where['orderField'] !== '' ? (string) $where['orderField'] : 'r.tier';
|
||||
$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')
|
||||
->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')
|
||||
->order($orderField, $orderType)
|
||||
->order('r.end_index', 'asc');
|
||||
|
||||
if ($tier !== '') {
|
||||
$query->where('r.tier', $tier);
|
||||
}
|
||||
->order('r.grid_number', 'asc');
|
||||
|
||||
$paginator = $query->paginate($limit, false, ['page' => $page]);
|
||||
$arr = $paginator->toArray();
|
||||
@@ -69,12 +93,47 @@ 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,并刷新缓存)
|
||||
* @param int $direction 0=顺时针 1=逆时针
|
||||
* @param array<int, array{id: int, weight: int}> $items id 为 end_index(DiceRewardConfig.id)
|
||||
*/
|
||||
public function batchUpdateWeightsByDirection(int $direction, array $items): void
|
||||
public function batchUpdateWeightsByDirection(int $direction, array $items, ?int $deptId = null): void
|
||||
{
|
||||
if (empty($items)) {
|
||||
return;
|
||||
@@ -90,23 +149,36 @@ class DiceRewardLogic
|
||||
}
|
||||
$weight = max(self::WEIGHT_MIN, min(self::WEIGHT_MAX, $weight));
|
||||
|
||||
$tier = DiceRewardConfig::where('id', $id)->value('tier');
|
||||
$configQuery = DiceRewardConfig::where('id', $id);
|
||||
if ($deptId !== null) {
|
||||
ConfigScopeEditHelper::applyDeptIdWhere($configQuery, $deptId);
|
||||
}
|
||||
$tier = $configQuery->value('tier');
|
||||
if ($tier === null || $tier === '') {
|
||||
throw new ApiException(\app\api\util\ApiLang::translateParams('配置ID %s 不存在或档位为空', [$id]));
|
||||
}
|
||||
$tier = (string) $tier;
|
||||
|
||||
$affected = DiceReward::where('tier', $tier)->where('direction', $direction)->where('end_index', $id)->update(['weight' => $weight]);
|
||||
$rewardQuery = DiceReward::where('tier', $tier)
|
||||
->where('direction', $direction)
|
||||
->where('end_index', $id);
|
||||
if ($deptId !== null) {
|
||||
ConfigScopeEditHelper::applyDeptIdWhere($rewardQuery, $deptId);
|
||||
}
|
||||
$affected = $rewardQuery->update(['weight' => $weight]);
|
||||
if ($affected === 0) {
|
||||
$m = new DiceReward();
|
||||
$m->tier = $tier;
|
||||
$m->direction = $direction;
|
||||
$m->end_index = $id;
|
||||
$m->weight = $weight;
|
||||
if ($deptId !== null && $deptId > 0) {
|
||||
$m->dept_id = $deptId;
|
||||
}
|
||||
$m->save();
|
||||
}
|
||||
}
|
||||
DiceReward::refreshCache();
|
||||
DiceReward::refreshCache($deptId);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -114,11 +186,11 @@ class DiceRewardLogic
|
||||
* @param int $direction 0=顺时针 1=逆时针
|
||||
* @return array<string, array> 键 T1|T2|...|BIGWIN,值为该档位下带 weight 的行数组
|
||||
*/
|
||||
public function getListGroupedByTierForDirection(int $direction): array
|
||||
public function getListGroupedByTierForDirection(int $direction, ?int $deptId = null): array
|
||||
{
|
||||
$configInstance = DiceRewardConfig::getCachedInstance();
|
||||
$configInstance = DiceRewardConfig::getCachedInstance($deptId);
|
||||
$byTier = $configInstance['by_tier'] ?? [];
|
||||
$rewardInstance = DiceReward::getCachedInstance();
|
||||
$rewardInstance = DiceReward::getCachedInstance($deptId);
|
||||
$byTierDirection = $rewardInstance['by_tier_direction'] ?? [];
|
||||
|
||||
$result = [];
|
||||
@@ -153,9 +225,9 @@ class DiceRewardLogic
|
||||
*
|
||||
* @return array<string, array{0: array, 1: array}>
|
||||
*/
|
||||
public function getListGroupedByTierWithDirection(): array
|
||||
public function getListGroupedByTierWithDirection(?int $deptId = null): array
|
||||
{
|
||||
$rewardInstance = DiceReward::getCachedInstance();
|
||||
$rewardInstance = DiceReward::getCachedInstance($deptId);
|
||||
$byTierDirection = $rewardInstance['by_tier_direction'] ?? [];
|
||||
|
||||
$result = [];
|
||||
@@ -185,7 +257,7 @@ class DiceRewardLogic
|
||||
* @param array<int, array{id: int, weight: int}> $items 每项 id 为 dice_reward 表主键,weight 为 1-10000
|
||||
* @throws ApiException
|
||||
*/
|
||||
public function batchUpdateWeights(array $items): void
|
||||
public function batchUpdateWeights(array $items, ?int $deptId = null): void
|
||||
{
|
||||
if (empty($items)) {
|
||||
return;
|
||||
@@ -203,13 +275,24 @@ class DiceRewardLogic
|
||||
}
|
||||
$weight = isset($item['weight']) ? (int) $item['weight'] : self::WEIGHT_MIN;
|
||||
$weight = max(self::WEIGHT_MIN, min(self::WEIGHT_MAX, $weight));
|
||||
$model = DiceReward::find($id);
|
||||
$query = DiceReward::where('id', $id);
|
||||
if ($deptId !== null) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
$model = $query->find();
|
||||
if ($model !== null) {
|
||||
$model->weight = $weight;
|
||||
$model->save();
|
||||
}
|
||||
}
|
||||
DiceReward::refreshCache();
|
||||
DiceReward::refreshCache($deptId);
|
||||
}
|
||||
|
||||
/** BIGWIN 权重范围:0=0% 中奖,10000=100% 中奖;grid_number=5/30 固定 100% 不可改 */
|
||||
@@ -219,9 +302,9 @@ class DiceRewardLogic
|
||||
* 按 grid_number 获取 BIGWIN 档位权重(取顺时针方向,用于编辑展示)
|
||||
* 若 DiceReward 无该点数则 5/30 返回 10000,其余返回 0
|
||||
*/
|
||||
public function getBigwinWeightByGridNumber(int $gridNumber): int
|
||||
public function getBigwinWeightByGridNumber(int $gridNumber, ?int $deptId = null): int
|
||||
{
|
||||
$inst = DiceReward::getCachedInstance();
|
||||
$inst = DiceReward::getCachedInstance($deptId);
|
||||
$rows = $inst['by_tier_direction']['BIGWIN'][DiceReward::DIRECTION_CLOCKWISE] ?? [];
|
||||
foreach ($rows as $row) {
|
||||
if ((int) ($row['grid_number'] ?? 0) === $gridNumber) {
|
||||
@@ -235,21 +318,24 @@ class DiceRewardLogic
|
||||
* 更新 BIGWIN 档位某点数的权重(顺/逆时针同时更新);0=0% 中奖,10000=100% 中奖
|
||||
* 表 dice_reward 唯一键为 (direction, grid_number),同一点数同一方向仅一条记录,故先按该键查找再更新,避免重复插入
|
||||
*/
|
||||
public function updateBigwinWeight(int $gridNumber, int $weight): void
|
||||
public function updateBigwinWeight(int $gridNumber, int $weight, ?int $deptId = null): void
|
||||
{
|
||||
$weight = min(self::BIGWIN_WEIGHT_MAX, max(0, $weight));
|
||||
$config = DiceRewardConfig::where('tier', 'BIGWIN')
|
||||
->where('grid_number', $gridNumber)
|
||||
->find();
|
||||
if ($deptId === null) {
|
||||
$deptId = AdminScopeHelper::DEFAULT_TEMPLATE_DEPT;
|
||||
}
|
||||
$configQuery = DiceRewardConfig::where('tier', 'BIGWIN')->where('grid_number', $gridNumber);
|
||||
ConfigScopeEditHelper::applyDeptIdWhere($configQuery, $deptId);
|
||||
$config = $configQuery->find();
|
||||
if (! $config) {
|
||||
return;
|
||||
}
|
||||
$configArr = $config->toArray();
|
||||
foreach ([DiceReward::DIRECTION_CLOCKWISE, DiceReward::DIRECTION_COUNTERCLOCKWISE] as $direction) {
|
||||
// 按唯一键 (direction, grid_number) 查找,存在则更新,不存在则插入
|
||||
$row = DiceReward::where('direction', $direction)
|
||||
->where('grid_number', $gridNumber)
|
||||
->find();
|
||||
$rowQuery = DiceReward::where('direction', $direction)->where('grid_number', $gridNumber);
|
||||
ConfigScopeEditHelper::applyDeptIdWhere($rowQuery, $deptId);
|
||||
$row = $rowQuery->find();
|
||||
if ($row) {
|
||||
$row->tier = 'BIGWIN';
|
||||
$row->weight = $weight > 0 ? $weight : self::WEIGHT_MIN;
|
||||
@@ -272,10 +358,13 @@ class DiceRewardLogic
|
||||
$m->remark = (string) ($configArr['remark'] ?? '');
|
||||
$m->type = $configArr['type'] ?? null;
|
||||
$m->weight = $weight > 0 ? $weight : self::WEIGHT_MIN;
|
||||
if (!AdminScopeHelper::isTemplateDeptId($deptId)) {
|
||||
$m->dept_id = $deptId;
|
||||
}
|
||||
$m->save();
|
||||
}
|
||||
}
|
||||
DiceReward::refreshCache();
|
||||
DiceReward::refreshCache($deptId);
|
||||
}
|
||||
|
||||
/** 盘面格数(用于顺时针/逆时针计算 end_index) */
|
||||
@@ -285,6 +374,94 @@ class DiceRewardLogic
|
||||
private const GRID_NUMBER_MIN = 5;
|
||||
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 生成记录。
|
||||
*
|
||||
@@ -297,9 +474,9 @@ class DiceRewardLogic
|
||||
* - 奖励档位:tier = DiceRewardConfig::where('id', $end_index)->first()->tier
|
||||
* - 显示ui:ui_text = DiceRewardConfig::where('id', $end_index)->first()->ui_text
|
||||
* - 实际中奖: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=大奖格)
|
||||
* - weight 默认 1,后续在权重编辑弹窗设置
|
||||
* - weight 默认:10/15/20/25 为 10,5/30 为 1,其余为 100
|
||||
*
|
||||
* 例如顺时针摇取点数为 5 时:start_index = 配置中 grid_number=5 对应格位的 id,
|
||||
* 结束位置 = (起始位置 + grid_number) % 26,再取该位置的 config 的 id 作为 end_index。
|
||||
@@ -309,27 +486,107 @@ class DiceRewardLogic
|
||||
* @return array{created_clockwise: int, created_counterclockwise: int, updated_clockwise: int, updated_counterclockwise: int, skipped: int}
|
||||
* @throws ApiException
|
||||
*/
|
||||
public function createRewardReferenceFromConfig(): array
|
||||
public function createRewardReferenceFromConfig(?int $deptId = null): array
|
||||
{
|
||||
$list = DiceRewardConfig::order('id', 'asc')->select()->toArray();
|
||||
if (empty($list)) {
|
||||
throw new ApiException('Reward config is empty, please maintain dice_reward_config first');
|
||||
}
|
||||
$configCount = count($list);
|
||||
if ($configCount < self::BOARD_SIZE) {
|
||||
throw new ApiException(
|
||||
\app\api\util\ApiLang::translateParams(
|
||||
'奖励配置需覆盖 26 个格位(id 0-25 或 1-26),当前仅 %s 条,无法完整生成 5-30 共26个点数、顺时针与逆时针的奖励对照',
|
||||
[$configCount]
|
||||
)
|
||||
);
|
||||
$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();
|
||||
Db::execute('DELETE FROM `' . $table . '`');
|
||||
DiceReward::refreshCache();
|
||||
if ($normalizedDeptId === null) {
|
||||
Db::table($table)->whereNull('dept_id')->delete();
|
||||
} else {
|
||||
Db::table($table)->where('dept_id', $normalizedDeptId)->delete();
|
||||
}
|
||||
|
||||
// 按 id 排序后,盘面位置 0..25 对应 $list[$pos],避免 config.id 非 0-25/1-26 时取模结果找不到
|
||||
$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');
|
||||
if ($deptId === null || $deptId === AdminScopeHelper::DEFAULT_TEMPLATE_DEPT) {
|
||||
$templateId = AdminScopeHelper::DEFAULT_TEMPLATE_DEPT;
|
||||
$configQuery->where(function ($q) use ($templateId) {
|
||||
$q->where('dept_id', $templateId)->whereOr('dept_id', 'null');
|
||||
});
|
||||
$deptId = null;
|
||||
} else {
|
||||
$configQuery->where('dept_id', $deptId);
|
||||
}
|
||||
$list = $configQuery->select()->toArray();
|
||||
if (empty($list)) {
|
||||
throw new ApiException('Reward config is empty, please maintain dice_reward_config first');
|
||||
}
|
||||
if (count($list) < self::BOARD_SIZE) {
|
||||
throw new ApiException(
|
||||
\app\api\util\ApiLang::translateParams(
|
||||
'奖励配置需覆盖 26 个格位(id 0-25 或 1-26),当前仅 %s 条,无法完整生成 5-30 共26个点数、顺时针与逆时针的奖励对照',
|
||||
[count($list)]
|
||||
)
|
||||
);
|
||||
}
|
||||
return $list;
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算 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 = [];
|
||||
foreach ($list as $pos => $row) {
|
||||
$gn = isset($row['grid_number']) ? (int) $row['grid_number'] : 0;
|
||||
@@ -338,12 +595,8 @@ class DiceRewardLogic
|
||||
}
|
||||
}
|
||||
|
||||
$createdCw = 0;
|
||||
$createdCcw = 0;
|
||||
$updatedCw = 0;
|
||||
$updatedCcw = 0;
|
||||
$rows = [];
|
||||
$skipped = 0;
|
||||
|
||||
for ($gridNumber = self::GRID_NUMBER_MIN; $gridNumber <= self::GRID_NUMBER_MAX; $gridNumber++) {
|
||||
if (!isset($gridToPosition[$gridNumber])) {
|
||||
$skipped++;
|
||||
@@ -358,97 +611,206 @@ class DiceRewardLogic
|
||||
|
||||
$configCw = $list[$endPosCw] ?? 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) {
|
||||
$tier = isset($configCw['tier']) ? trim((string) $configCw['tier']) : '';
|
||||
if ($tier !== '') {
|
||||
// 使用对应奖励配置的 weight 作为格子权重(若未配置则退回最小权重)
|
||||
$weightCw = isset($configCw['weight']) && $configCw['weight'] !== null
|
||||
? $configCw['weight']
|
||||
: self::WEIGHT_MIN;
|
||||
$payloadCw = [
|
||||
'tier' => $tier,
|
||||
'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,
|
||||
];
|
||||
$existing = DiceReward::where('direction', DiceReward::DIRECTION_CLOCKWISE)->where('grid_number', $gridNumber)->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;
|
||||
$m->save();
|
||||
$createdCw++;
|
||||
$rows[] = $this->buildReferenceRowFromLandingConfig(
|
||||
$tier,
|
||||
$configCw,
|
||||
DiceReward::DIRECTION_CLOCKWISE,
|
||||
$gridNumber,
|
||||
$startId
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($configCcw !== null) {
|
||||
$tier = isset($configCcw['tier']) ? trim((string) $configCcw['tier']) : '';
|
||||
if ($tier !== '') {
|
||||
// 使用对应奖励配置的 weight 作为格子权重(若未配置则退回最小权重)
|
||||
$weightCcw = isset($configCcw['weight']) && $configCcw['weight'] !== null
|
||||
? $configCcw['weight']
|
||||
: self::WEIGHT_MIN;
|
||||
$payloadCcw = [
|
||||
'tier' => $tier,
|
||||
'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,
|
||||
];
|
||||
$existing = DiceReward::where('direction', DiceReward::DIRECTION_COUNTERCLOCKWISE)->where('grid_number', $gridNumber)->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;
|
||||
$m->save();
|
||||
$createdCcw++;
|
||||
$rows[] = $this->buildReferenceRowFromLandingConfig(
|
||||
$tier,
|
||||
$configCcw,
|
||||
DiceReward::DIRECTION_COUNTERCLOCKWISE,
|
||||
$gridNumber,
|
||||
$startId
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
return ['rows' => $rows, 'skipped' => $skipped];
|
||||
}
|
||||
|
||||
DiceReward::refreshCache();
|
||||
/**
|
||||
* 对照表落点行:档位按落点格 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 [
|
||||
'created_clockwise' => $createdCw,
|
||||
'created_counterclockwise' => $createdCcw,
|
||||
'updated_clockwise' => $updatedCw,
|
||||
'updated_counterclockwise' => $updatedCcw,
|
||||
'skipped' => $skipped,
|
||||
'tier' => $tier,
|
||||
'direction' => $direction,
|
||||
'weight' => $this->defaultReferenceWeightForGrid($gridNumber),
|
||||
'grid_number' => $gridNumber,
|
||||
'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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,11 +6,13 @@
|
||||
// +----------------------------------------------------------------------
|
||||
namespace app\dice\logic\reward_config;
|
||||
|
||||
use app\dice\helper\AdminScopeHelper;
|
||||
use app\dice\helper\ConfigScopeEditHelper;
|
||||
use app\dice\logic\reward\DiceRewardLogic;
|
||||
use app\dice\model\lottery_pool_config\DiceLotteryPoolConfig;
|
||||
use app\dice\model\reward\DiceRewardConfig;
|
||||
use app\dice\model\reward_config_record\DiceRewardConfigRecord;
|
||||
use plugin\saiadmin\basic\think\BaseLogic;
|
||||
use app\dice\basic\DiceBaseLogic;
|
||||
use plugin\saiadmin\exception\ApiException;
|
||||
use plugin\saiadmin\utils\Helper;
|
||||
use support\Log;
|
||||
@@ -19,7 +21,7 @@ use support\Log;
|
||||
* 奖励配置逻辑层(DiceRewardConfig)
|
||||
* weight 1-10000,各档位权重和不限制
|
||||
*/
|
||||
class DiceRewardConfigLogic extends BaseLogic
|
||||
class DiceRewardConfigLogic extends DiceBaseLogic
|
||||
{
|
||||
/** weight 取值范围 */
|
||||
private const WEIGHT_MIN = 1;
|
||||
@@ -36,18 +38,23 @@ class DiceRewardConfigLogic extends BaseLogic
|
||||
public function add(array $data): mixed
|
||||
{
|
||||
$result = parent::add($data);
|
||||
DiceRewardConfig::refreshCache();
|
||||
$deptId = AdminScopeHelper::normalizeRecordDeptId($data['dept_id'] ?? null);
|
||||
DiceRewardConfig::refreshCache($deptId);
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改:保存后刷新缓存;BIGWIN 的 weight 直接写入 dice_reward_config 表,抽奖时从 Config 读取
|
||||
* 修改:按业务 id + 渠道更新;保存后刷新该渠道缓存
|
||||
*/
|
||||
public function edit($id, array $data): mixed
|
||||
public function edit($id, array $data, ?array $adminInfo = null, $requestDeptId = null): mixed
|
||||
{
|
||||
$result = parent::edit($id, $data);
|
||||
DiceRewardConfig::refreshCache();
|
||||
return $result;
|
||||
$deptId = AdminScopeHelper::resolveConfigDeptId(
|
||||
$adminInfo,
|
||||
AdminScopeHelper::pickRequestDeptId($requestDeptId, $data)
|
||||
);
|
||||
ConfigScopeEditHelper::updateByBusinessIdAndDept($this->model, (int) $id, $deptId, $data);
|
||||
DiceRewardConfig::refreshCache($deptId);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -152,8 +159,9 @@ class DiceRewardConfigLogic extends BaseLogic
|
||||
/**
|
||||
* 批量更新奖励索引配置:grid_number、ui_text、real_ev、tier、remark(不含 weight,BIGWIN 权重单独接口)
|
||||
* @param array $items 每项 [id, grid_number?, ui_text?, real_ev?, tier?, remark?]
|
||||
* @param int $deptId 渠道 ID(0=默认模板)
|
||||
*/
|
||||
public function batchUpdate(array $items): void
|
||||
public function batchUpdate(array $items, int $deptId = AdminScopeHelper::DEFAULT_TEMPLATE_DEPT): void
|
||||
{
|
||||
foreach ($items as $row) {
|
||||
if (! array_key_exists('id', $row) || $row['id'] === null || $row['id'] === '') {
|
||||
@@ -167,10 +175,18 @@ class DiceRewardConfigLogic extends BaseLogic
|
||||
}
|
||||
}
|
||||
if (! empty($data)) {
|
||||
parent::edit($id, $data);
|
||||
$this->updateByBusinessIdAndDept($id, $deptId, $data);
|
||||
}
|
||||
}
|
||||
DiceRewardConfig::refreshCache();
|
||||
DiceRewardConfig::refreshCache($deptId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 按业务 id(0~25)与渠道更新单条配置
|
||||
*/
|
||||
private function updateByBusinessIdAndDept(int $businessId, int $deptId, array $data): void
|
||||
{
|
||||
ConfigScopeEditHelper::updateByBusinessIdAndDept($this->model, $businessId, $deptId, $data);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -201,8 +217,9 @@ class DiceRewardConfigLogic extends BaseLogic
|
||||
/**
|
||||
* 批量更新 BIGWIN 档位权重(仅写 dice_reward_config 表,不操作 dice_reward)
|
||||
* @param array $items 每项 [grid_number => 5-30, weight => 0-10000]
|
||||
* @param int $deptId 渠道 ID(0=默认模板)
|
||||
*/
|
||||
public function batchUpdateBigwinWeight(array $items): void
|
||||
public function batchUpdateBigwinWeight(array $items, int $deptId = AdminScopeHelper::DEFAULT_TEMPLATE_DEPT): void
|
||||
{
|
||||
$weightMin = 0;
|
||||
$weightMax = 10000;
|
||||
@@ -213,21 +230,33 @@ class DiceRewardConfigLogic extends BaseLogic
|
||||
continue;
|
||||
}
|
||||
$weight = max($weightMin, min($weightMax, $weight));
|
||||
$this->model->where('tier', 'BIGWIN')
|
||||
->where('grid_number', $gridNumber)
|
||||
->update(['weight' => $weight]);
|
||||
$query = $this->model->where('tier', 'BIGWIN')->where('grid_number', $gridNumber);
|
||||
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);
|
||||
}
|
||||
DiceRewardConfig::refreshCache();
|
||||
$exists = (clone $query)->find();
|
||||
if ($exists === null) {
|
||||
throw new ApiException('BIGWIN grid_number=' . $gridNumber . ' not found for current channel');
|
||||
}
|
||||
$query->update(['weight' => $weight]);
|
||||
}
|
||||
DiceRewardConfig::refreshCache($deptId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除后刷新缓存
|
||||
*/
|
||||
public function destroy($ids): bool
|
||||
public function destroy($ids, ?array $adminInfo = null, $requestDeptId = null): bool
|
||||
{
|
||||
$deptId = AdminScopeHelper::resolveConfigDeptId($adminInfo, $requestDeptId);
|
||||
$result = parent::destroy($ids);
|
||||
if ($result) {
|
||||
DiceRewardConfig::refreshCache();
|
||||
DiceRewardConfig::refreshCache($deptId);
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
@@ -403,6 +432,12 @@ class DiceRewardConfigLogic extends BaseLogic
|
||||
$record->lottery_config_id = $config ? (int) $config->id : null;
|
||||
$record->result_counts = $counts;
|
||||
$record->admin_id = $adminId;
|
||||
if ($adminId > 0) {
|
||||
$admin = \plugin\saiadmin\app\model\system\SystemUser::find($adminId);
|
||||
if ($admin && !empty($admin->dept_id)) {
|
||||
$record->dept_id = $admin->dept_id;
|
||||
}
|
||||
}
|
||||
$record->create_time = date('Y-m-d H:i:s');
|
||||
$record->save();
|
||||
$recordId = (int) $record->id;
|
||||
|
||||
@@ -4,12 +4,16 @@
|
||||
// +----------------------------------------------------------------------
|
||||
namespace app\dice\logic\reward_config_record;
|
||||
|
||||
use app\dice\model\lottery_pool_config\DiceLotteryPoolConfig;
|
||||
use app\api\util\ApiLang;
|
||||
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\reward\DiceReward;
|
||||
use app\dice\model\reward\DiceRewardConfig;
|
||||
use app\dice\model\play_record_test\DicePlayRecordTest;
|
||||
use app\dice\model\reward_config_record\DiceRewardConfigRecord;
|
||||
use plugin\saiadmin\basic\think\BaseLogic;
|
||||
use app\dice\basic\DiceBaseLogic;
|
||||
use plugin\saiadmin\exception\ApiException;
|
||||
use plugin\saiadmin\app\model\system\SystemUser;
|
||||
|
||||
@@ -17,7 +21,7 @@ use plugin\saiadmin\app\model\system\SystemUser;
|
||||
* 奖励配置权重测试记录逻辑层
|
||||
*
|
||||
*/
|
||||
class DiceRewardConfigRecordLogic extends BaseLogic
|
||||
class DiceRewardConfigRecordLogic extends DiceBaseLogic
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
@@ -74,6 +78,51 @@ class DiceRewardConfigRecordLogic extends BaseLogic
|
||||
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)
|
||||
* @param int $recordId 测试记录 ID
|
||||
@@ -88,6 +137,7 @@ class DiceRewardConfigRecordLogic extends BaseLogic
|
||||
throw new ApiException('Test record not found');
|
||||
}
|
||||
$record = is_array($record) ? $record : $record->toArray();
|
||||
$configDeptId = AdminScopeHelper::normalizeRecordDeptId($record['dept_id'] ?? null);
|
||||
|
||||
$snapshot = $record['weight_config_snapshot'] ?? null;
|
||||
if (is_string($snapshot)) {
|
||||
@@ -112,9 +162,9 @@ class DiceRewardConfigRecordLogic extends BaseLogic
|
||||
$tier = $tierFromDb !== null ? (string) $tierFromDb : '';
|
||||
}
|
||||
// 仅按方向 + 点数更新 DiceReward(若存在则更新,不存在才插入,避免唯一键冲突)
|
||||
$reward = DiceReward::where('direction', $direction)
|
||||
->where('grid_number', $gridNumber)
|
||||
->find();
|
||||
$rewardQuery = DiceReward::where('direction', $direction)->where('grid_number', $gridNumber);
|
||||
ConfigScopeEditHelper::applyDeptIdWhere($rewardQuery, $configDeptId);
|
||||
$reward = $rewardQuery->find();
|
||||
if ($reward) {
|
||||
$reward->weight = $weight;
|
||||
// 若快照中有 tier,补齐 tier 信息
|
||||
@@ -130,10 +180,13 @@ class DiceRewardConfigRecordLogic extends BaseLogic
|
||||
$m->direction = $direction;
|
||||
$m->grid_number = $gridNumber;
|
||||
$m->weight = $weight;
|
||||
if (!AdminScopeHelper::isTemplateDeptId($configDeptId)) {
|
||||
$m->dept_id = $configDeptId;
|
||||
}
|
||||
$m->save();
|
||||
}
|
||||
}
|
||||
DiceReward::refreshCache();
|
||||
DiceReward::refreshCache($configDeptId);
|
||||
}
|
||||
|
||||
// 使用记录中的 bigwin_weight JSON 将 BIGWIN 概率导入到 DiceRewardConfig
|
||||
@@ -152,11 +205,11 @@ class DiceRewardConfigRecordLogic extends BaseLogic
|
||||
if ($weight < 0) {
|
||||
$weight = 0;
|
||||
}
|
||||
DiceRewardConfig::where('tier', 'BIGWIN')
|
||||
->where('grid_number', $gridNumber)
|
||||
->update(['weight' => $weight]);
|
||||
$bigwinQuery = DiceRewardConfig::where('tier', 'BIGWIN')->where('grid_number', $gridNumber);
|
||||
ConfigScopeEditHelper::applyDeptIdWhere($bigwinQuery, $configDeptId);
|
||||
$bigwinQuery->update(['weight' => $weight]);
|
||||
}
|
||||
DiceRewardConfig::refreshCache();
|
||||
DiceRewardConfig::refreshCache($configDeptId);
|
||||
}
|
||||
|
||||
$tiers = ['T1', 'T2', 'T3', 'T4', 'T5'];
|
||||
@@ -225,7 +278,8 @@ class DiceRewardConfigRecordLogic extends BaseLogic
|
||||
DiceLotteryPoolConfig::where('id', $freeTargetId)->update($update);
|
||||
}
|
||||
|
||||
DiceRewardConfig::refreshCache();
|
||||
DiceRewardConfig::refreshCache($configDeptId);
|
||||
DiceReward::refreshCache($configDeptId);
|
||||
DiceRewardConfig::clearRequestInstance();
|
||||
}
|
||||
|
||||
@@ -237,8 +291,12 @@ class DiceRewardConfigRecordLogic extends BaseLogic
|
||||
* @return int 记录 ID
|
||||
* @throws ApiException
|
||||
*/
|
||||
public function createWeightTestRecord(array|int $params, mixed $adminIdOrFreeS = null, mixed $freeSOrFreeN = null, mixed $freeN = null): int
|
||||
{
|
||||
public function createWeightTestRecord(
|
||||
array|int $params,
|
||||
mixed $adminIdOrFreeS = null,
|
||||
?array $adminInfo = null,
|
||||
$requestDeptId = null
|
||||
): int {
|
||||
$adminId = null;
|
||||
if (!is_array($params)) {
|
||||
// 兼容旧版调用:createWeightTestRecord(paid_s_count, paid_n_count)
|
||||
@@ -249,15 +307,15 @@ class DiceRewardConfigRecordLogic extends BaseLogic
|
||||
} else {
|
||||
$adminId = $adminIdOrFreeS !== null && $adminIdOrFreeS !== '' ? (int) $adminIdOrFreeS : null;
|
||||
}
|
||||
|
||||
$deptId = $this->resolveWeightTestDeptId(
|
||||
$adminInfo,
|
||||
AdminScopeHelper::pickRequestDeptId($requestDeptId, is_array($params) ? $params : []),
|
||||
is_array($params) ? $params : []
|
||||
);
|
||||
$allowed = [100, 500, 1000, 5000];
|
||||
$ante = isset($params['ante']) ? intval($params['ante']) : 1;
|
||||
if ($ante <= 0) {
|
||||
throw new ApiException('ante must be greater than 0');
|
||||
}
|
||||
$anteExists = DiceAnteConfig::where('mult', $ante)->count();
|
||||
if ($anteExists <= 0) {
|
||||
throw new ApiException('ante not allowed: ' . $ante);
|
||||
}
|
||||
$anteRandom = !empty($params['ante_random']);
|
||||
$ante = $anteRandom ? 0 : $this->resolveWeightTestAnte($params, $deptId);
|
||||
|
||||
$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;
|
||||
@@ -272,7 +330,12 @@ class DiceRewardConfigRecordLogic extends BaseLogic
|
||||
$paidN = isset($params['paid_n_count']) ? (int) $params['paid_n_count'] : 0;
|
||||
$chainFreeMode = !empty($params['chain_free_mode']);
|
||||
$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) {
|
||||
throw new ApiException('test_safety_line must be greater than or equal to 0');
|
||||
}
|
||||
@@ -296,8 +359,8 @@ class DiceRewardConfigRecordLogic extends BaseLogic
|
||||
$paidTierWeights = null;
|
||||
$freeTierWeights = null;
|
||||
|
||||
// 来自 DiceReward 的当前权重快照(按方向+点数),用于权重测试模拟
|
||||
$instance = DiceReward::getCachedInstance();
|
||||
// 来自当前渠道的 DiceReward 权重快照(按方向+点数),用于权重测试模拟
|
||||
$instance = DiceReward::getCachedInstance($deptId);
|
||||
$byTierDirection = $instance['by_tier_direction'] ?? [];
|
||||
foreach ($byTierDirection as $tier => $byDir) {
|
||||
foreach ($byDir as $dir => $rows) {
|
||||
@@ -316,7 +379,7 @@ class DiceRewardConfigRecordLogic extends BaseLogic
|
||||
// BIGWIN 概率快照从 DiceRewardConfig 读取(例如豹子号配置)
|
||||
// JSON 结构 {"grid_number": weight, ...}
|
||||
$bigwinWeights = [];
|
||||
$bigwinConfigs = DiceRewardConfig::getCachedByTier('BIGWIN');
|
||||
$bigwinConfigs = DiceRewardConfig::getCachedByTier('BIGWIN', $deptId);
|
||||
foreach ($bigwinConfigs as $cfg) {
|
||||
$grid = isset($cfg['grid_number']) ? (int) $cfg['grid_number'] : 0;
|
||||
if ($grid <= 0) {
|
||||
@@ -327,10 +390,7 @@ class DiceRewardConfigRecordLogic extends BaseLogic
|
||||
}
|
||||
|
||||
if ($paidConfigId > 0) {
|
||||
$config = DiceLotteryPoolConfig::find($paidConfigId);
|
||||
if (!$config) {
|
||||
throw new ApiException('Paid pool config not found');
|
||||
}
|
||||
$config = $this->findPoolConfigInDept($paidConfigId, $deptId, 'Paid pool config not found');
|
||||
$tierWeightsSnapshot['paid'] = [
|
||||
'T1' => (int) ($config->t1_weight ?? 0),
|
||||
'T2' => (int) ($config->t2_weight ?? 0),
|
||||
@@ -359,10 +419,7 @@ class DiceRewardConfigRecordLogic extends BaseLogic
|
||||
}
|
||||
|
||||
if ($freeConfigId > 0) {
|
||||
$config = DiceLotteryPoolConfig::find($freeConfigId);
|
||||
if (!$config) {
|
||||
throw new ApiException('Free pool config not found');
|
||||
}
|
||||
$config = $this->findPoolConfigInDept($freeConfigId, $deptId, 'Free pool config not found');
|
||||
$tierWeightsSnapshot['free'] = [
|
||||
'T1' => (int) ($config->t1_weight ?? 0),
|
||||
'T2' => (int) ($config->t2_weight ?? 0),
|
||||
@@ -400,6 +457,9 @@ class DiceRewardConfigRecordLogic extends BaseLogic
|
||||
if ($chainFreeMode) {
|
||||
$tierWeightsSnapshot['chain_free_mode'] = true;
|
||||
}
|
||||
if (!empty($params['ante_random'])) {
|
||||
$tierWeightsSnapshot['ante_random'] = true;
|
||||
}
|
||||
|
||||
$record = new DiceRewardConfigRecord();
|
||||
$plannedPaidSpins = $paidS + $paidN;
|
||||
@@ -428,9 +488,109 @@ class DiceRewardConfigRecordLogic extends BaseLogic
|
||||
$record->bigwin_weight = $bigwinWeights ?: null;
|
||||
$record->ante = $ante;
|
||||
$record->admin_id = $adminId;
|
||||
$record->dept_id = $deptId;
|
||||
$record->create_time = date('Y-m-d H:i:s');
|
||||
$record->save();
|
||||
|
||||
return (int) $record->id;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析一键测试所属渠道:请求 dept_id > 奖池配置 dept_id > 渠道管理员本渠道
|
||||
*/
|
||||
private function resolveWeightTestDeptId(?array $adminInfo, $requestDeptId, array $params): int
|
||||
{
|
||||
$deptId = AdminScopeHelper::resolveConfigDeptId($adminInfo, $requestDeptId);
|
||||
if (! AdminScopeHelper::isTemplateDeptId($deptId)) {
|
||||
return $deptId;
|
||||
}
|
||||
|
||||
foreach (['paid_lottery_config_id', 'free_lottery_config_id', 'lottery_config_id'] as $key) {
|
||||
$poolId = isset($params[$key]) ? (int) $params[$key] : 0;
|
||||
if ($poolId <= 0) {
|
||||
continue;
|
||||
}
|
||||
$pool = DiceLotteryPoolConfig::find($poolId);
|
||||
if (! $pool) {
|
||||
continue;
|
||||
}
|
||||
$poolDeptId = AdminScopeHelper::normalizeRecordDeptId($pool->dept_id ?? null);
|
||||
if (! AdminScopeHelper::isTemplateDeptId($poolDeptId)) {
|
||||
return $poolDeptId;
|
||||
}
|
||||
}
|
||||
|
||||
$scopeDeptId = AdminScopeHelper::getDeptId($adminInfo);
|
||||
if ($scopeDeptId !== null && $scopeDeptId > 0) {
|
||||
return $scopeDeptId;
|
||||
}
|
||||
|
||||
return $deptId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验奖池配置属于当前渠道
|
||||
*/
|
||||
private function findPoolConfigInDept(int $poolId, int $deptId, string $notFoundMsg): DiceLotteryPoolConfig
|
||||
{
|
||||
$config = DiceLotteryPoolConfig::find($poolId);
|
||||
if (!$config) {
|
||||
throw new ApiException($notFoundMsg);
|
||||
}
|
||||
$poolDeptId = AdminScopeHelper::normalizeRecordDeptId($config->dept_id ?? null);
|
||||
if ($poolDeptId !== $deptId) {
|
||||
throw new ApiException('POOL_CONFIG_NOT_IN_CHANNEL');
|
||||
}
|
||||
return $config;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析一键测试底注:优先 ante_config_id,否则按 mult + 渠道校验
|
||||
*/
|
||||
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;
|
||||
if ($anteConfigId > 0) {
|
||||
$config = DiceAnteConfig::find($anteConfigId);
|
||||
if (! $config) {
|
||||
throw new ApiException('ANTE_CONFIG_NOT_FOUND');
|
||||
}
|
||||
$configDeptId = AdminScopeHelper::normalizeRecordDeptId($config->dept_id ?? null);
|
||||
if ($configDeptId !== $deptId) {
|
||||
throw new ApiException('ANTE_CONFIG_NOT_IN_CHANNEL');
|
||||
}
|
||||
$mult = (int) ($config->mult ?? 0);
|
||||
if ($mult <= 0) {
|
||||
throw new ApiException('ANTE_MUST_POSITIVE');
|
||||
}
|
||||
return $mult;
|
||||
}
|
||||
|
||||
$ante = isset($params['ante']) ? (int) $params['ante'] : 0;
|
||||
if ($ante <= 0) {
|
||||
throw new ApiException('ANTE_MUST_POSITIVE');
|
||||
}
|
||||
$anteQuery = DiceAnteConfig::where('mult', $ante);
|
||||
ConfigScopeEditHelper::applyDeptIdWhere($anteQuery, $deptId);
|
||||
if ($anteQuery->count() <= 0) {
|
||||
throw new ApiException(ApiLang::translateParams('ANTE_NOT_ALLOWED', [$ante]));
|
||||
}
|
||||
|
||||
return $ante;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,9 @@ declare(strict_types=1);
|
||||
namespace app\dice\logic\reward_config_record;
|
||||
|
||||
use app\api\logic\PlayStartLogic;
|
||||
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\play_record_test\DicePlayRecordTest;
|
||||
use app\dice\model\reward_config_record\DiceRewardConfigRecord;
|
||||
@@ -14,14 +17,21 @@ use support\think\Db;
|
||||
|
||||
/**
|
||||
* 一键测试权重:单进程后台执行模拟摇色子,写入 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
|
||||
{
|
||||
private const BATCH_SIZE = 10;
|
||||
|
||||
/** 本次测试所属渠道(与 dice_reward_config_record.dept_id 一致) */
|
||||
private int $runDeptId = 0;
|
||||
/** 测试记录写库白名单字段 */
|
||||
private const PLAY_RECORD_TEST_COLUMNS = [
|
||||
'reward_config_record_id',
|
||||
'dept_id',
|
||||
'admin_id',
|
||||
'lottery_config_id',
|
||||
'lottery_type',
|
||||
@@ -52,7 +62,6 @@ class WeightTestRunner
|
||||
return;
|
||||
}
|
||||
|
||||
$ante = is_numeric($record->ante ?? null) ? intval($record->ante) : 1;
|
||||
$paidS = (int) ($record->paid_s_count ?? 0);
|
||||
$paidN = (int) ($record->paid_n_count ?? 0);
|
||||
$total = $paidS + $paidN;
|
||||
@@ -61,10 +70,22 @@ class WeightTestRunner
|
||||
return;
|
||||
}
|
||||
|
||||
$configType0 = DiceLotteryPoolConfig::where('name', 'default')->find();
|
||||
$configType1 = DiceLotteryPoolConfig::where('name', 'killScore')->find();
|
||||
$this->runDeptId = $this->resolveRunDeptId($recordId, $record);
|
||||
$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);
|
||||
DiceRewardConfig::clearRequestInstance();
|
||||
|
||||
$configType0 = DiceLotteryPoolConfig::findByNameForDept('default', $deptId);
|
||||
$configKill = DiceLotteryPoolConfig::findByNameForDept('killScore', $deptId);
|
||||
$configFree = DiceLotteryPoolConfig::findByNameForDept('free', $deptId);
|
||||
if (!$configType0) {
|
||||
$this->markFailed($recordId, '彩金池配置 name=default 不存在');
|
||||
$this->markFailed($recordId, '彩金池配置 name=default 不存在(当前渠道)');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -79,12 +100,12 @@ class WeightTestRunner
|
||||
$freePoolConfigId = (int) ($record->free_lottery_config_id ?? 0);
|
||||
|
||||
$paidPoolConfig = $paidPoolConfigId > 0 ? DiceLotteryPoolConfig::find($paidPoolConfigId) : $configType0;
|
||||
if (!$paidPoolConfig) {
|
||||
if (!$paidPoolConfig || AdminScopeHelper::normalizeRecordDeptId($paidPoolConfig->dept_id ?? null) !== $deptId) {
|
||||
$paidPoolConfig = $configType0;
|
||||
}
|
||||
$freePoolConfig = $freePoolConfigId > 0 ? DiceLotteryPoolConfig::find($freePoolConfigId) : $configType1;
|
||||
if (!$freePoolConfig) {
|
||||
$freePoolConfig = $configType0;
|
||||
$freePoolConfig = $freePoolConfigId > 0 ? DiceLotteryPoolConfig::find($freePoolConfigId) : $configFree;
|
||||
if (!$freePoolConfig || AdminScopeHelper::normalizeRecordDeptId($freePoolConfig->dept_id ?? null) !== $deptId) {
|
||||
$freePoolConfig = $configFree ?: $configType0;
|
||||
}
|
||||
|
||||
if ($paidTierWeightsCustom !== null && array_sum($paidTierWeightsCustom) <= 0) {
|
||||
@@ -106,8 +127,11 @@ class WeightTestRunner
|
||||
$testSafetyLine = 0;
|
||||
}
|
||||
|
||||
// 测试内“玩家累计盈利”:用于控制付费局是否切换杀分
|
||||
$playerProfitTotal = 0.0;
|
||||
// 彩金池累计盈利:从 default.profit_amount 起步并在测试内逐局累加,与杀分判定值一致
|
||||
$poolProfitTotal = (float) ($configType0->profit_amount ?? 0);
|
||||
|
||||
// 付费未杀分时的模拟玩家档位权重(自定义 > 快照 > 兜底奖池)
|
||||
$paidPlayerWeights = $paidTierWeightsCustom ?? $this->resolveTierWeightsSnapshot($record, 'paid');
|
||||
|
||||
$playLogic = new PlayStartLogic();
|
||||
$resultCounts = [];
|
||||
@@ -118,18 +142,21 @@ class WeightTestRunner
|
||||
try {
|
||||
$this->runChainFreeMode(
|
||||
$recordId,
|
||||
$deptId,
|
||||
$playLogic,
|
||||
$paidS,
|
||||
$paidN,
|
||||
$ante,
|
||||
$anteRandom,
|
||||
$configType0,
|
||||
$paidPoolConfig,
|
||||
$freePoolConfig,
|
||||
$configType1,
|
||||
$paidTierWeightsCustom,
|
||||
$configKill,
|
||||
$paidPlayerWeights,
|
||||
$freeTierWeightsCustom,
|
||||
$killModeEnabled,
|
||||
$testSafetyLine,
|
||||
$playerProfitTotal,
|
||||
$poolProfitTotal,
|
||||
$resultCounts,
|
||||
$tierCounts,
|
||||
$buffer,
|
||||
@@ -145,6 +172,9 @@ class WeightTestRunner
|
||||
} catch (\Throwable $e) {
|
||||
Log::error('WeightTestRunner exception: ' . $e->getMessage(), ['record_id' => $recordId, 'trace' => $e->getTraceAsString()]);
|
||||
$this->markFailed($recordId, $e->getMessage());
|
||||
} finally {
|
||||
DiceReward::clearRequestInstance();
|
||||
DiceRewardConfig::clearRequestInstance();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -153,18 +183,21 @@ class WeightTestRunner
|
||||
*/
|
||||
private function runChainFreeMode(
|
||||
int $recordId,
|
||||
int $deptId,
|
||||
PlayStartLogic $playLogic,
|
||||
int $paidS,
|
||||
int $paidN,
|
||||
int $ante,
|
||||
bool $anteRandom,
|
||||
$defaultPoolConfig,
|
||||
$paidPoolConfig,
|
||||
$freePoolConfig,
|
||||
$killPoolConfig,
|
||||
?array $paidTierWeightsCustom,
|
||||
?array $paidPlayerWeights,
|
||||
?array $freeTierWeightsCustom,
|
||||
bool $killModeEnabled,
|
||||
int $testSafetyLine,
|
||||
float &$playerProfitTotal,
|
||||
float &$poolProfitTotal,
|
||||
array &$resultCounts,
|
||||
array &$tierCounts,
|
||||
array &$buffer,
|
||||
@@ -172,39 +205,61 @@ class WeightTestRunner
|
||||
): void {
|
||||
$queue = [];
|
||||
for ($i = 0; $i < $paidS; $i++) {
|
||||
$queue[] = ['paid', 0, $ante];
|
||||
$queue[] = ['paid', 0, $anteRandom ? 0 : $ante];
|
||||
}
|
||||
for ($i = 0; $i < $paidN; $i++) {
|
||||
$queue[] = ['paid', 1, $ante];
|
||||
$queue[] = ['paid', 1, $anteRandom ? 0 : $ante];
|
||||
}
|
||||
$qi = 0;
|
||||
$lastPaidPlayAnte = 0;
|
||||
while ($qi < count($queue)) {
|
||||
$item = $queue[$qi];
|
||||
$isPaid = $item[0] === 'paid';
|
||||
$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;
|
||||
|
||||
if ($isPaid) {
|
||||
$useKillForPaid = $killModeEnabled && $playerProfitTotal >= $testSafetyLine && $killPoolConfig !== null;
|
||||
$useKillForPaid = $killModeEnabled
|
||||
&& $poolProfitTotal >= $testSafetyLine
|
||||
&& $killPoolConfig !== null;
|
||||
if ($useKillForPaid) {
|
||||
$cfg = $killPoolConfig;
|
||||
$customWeights = null;
|
||||
} else {
|
||||
// 付费未杀分:模拟玩家档位权重,lottery_config_id 记 default(与 PlayStartLogic 一致)
|
||||
$cfg = $defaultPoolConfig;
|
||||
$customWeights = $paidPlayerWeights;
|
||||
if ($customWeights === null) {
|
||||
$cfg = $paidPoolConfig;
|
||||
$customWeights = $paidTierWeightsCustom;
|
||||
$customWeights = null;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$cfg = $freePoolConfig;
|
||||
$customWeights = $freeTierWeightsCustom;
|
||||
}
|
||||
|
||||
$row = $playLogic->simulateOnePlay($cfg, $dir, $lotteryType, $playAnte, $customWeights);
|
||||
$row = $playLogic->simulateOnePlay($cfg, $dir, $lotteryType, $playAnte, $customWeights, $deptId);
|
||||
// 明细底注必须为 dice_ante_config.mult(随机模式每局独立抽取后的值)
|
||||
$row['ante'] = $playAnte;
|
||||
$winCoin = (float) ($row['win_coin'] ?? 0);
|
||||
$paidAmount = (float) ($row['paid_amount'] ?? 0);
|
||||
$playerProfitTotal += $winCoin - $paidAmount;
|
||||
$perPlayProfit = $isPaid ? ($winCoin - $paidAmount) : $winCoin;
|
||||
$poolProfitTotal += round($perPlayProfit, 2);
|
||||
$this->aggregate($row, $resultCounts, $tierCounts);
|
||||
$buffer[] = $this->rowForInsert($row, $recordId);
|
||||
$buffer[] = $this->rowForInsert($row, $recordId, $deptId);
|
||||
$done++;
|
||||
|
||||
if (!empty($row['grants_free_ticket'])) {
|
||||
@@ -217,6 +272,189 @@ 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 时读不到
|
||||
*/
|
||||
private function resolveRunDeptId(int $recordId, DiceRewardConfigRecord $record): int
|
||||
{
|
||||
$recordTable = (new DiceRewardConfigRecord())->getTable();
|
||||
$fromDb = Db::table($recordTable)->where('id', $recordId)->value('dept_id');
|
||||
$deptId = AdminScopeHelper::normalizeRecordDeptId($fromDb);
|
||||
if (! AdminScopeHelper::isTemplateDeptId($deptId)) {
|
||||
return $deptId;
|
||||
}
|
||||
|
||||
$deptId = AdminScopeHelper::normalizeRecordDeptId($record->dept_id ?? null);
|
||||
if (! AdminScopeHelper::isTemplateDeptId($deptId)) {
|
||||
return $deptId;
|
||||
}
|
||||
|
||||
return $this->resolveDeptIdFromRecordPools($record, $deptId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 历史记录 dept_id=0 时,从关联奖池配置反推并回写
|
||||
*/
|
||||
private function resolveDeptIdFromRecordPools(DiceRewardConfigRecord $record, int $fallbackDeptId): int
|
||||
{
|
||||
foreach (
|
||||
[
|
||||
(int) ($record->paid_lottery_config_id ?? 0),
|
||||
(int) ($record->free_lottery_config_id ?? 0),
|
||||
(int) ($record->lottery_config_id ?? 0),
|
||||
] as $poolId
|
||||
) {
|
||||
if ($poolId <= 0) {
|
||||
continue;
|
||||
}
|
||||
$pool = DiceLotteryPoolConfig::find($poolId);
|
||||
if (! $pool) {
|
||||
continue;
|
||||
}
|
||||
$poolDeptId = AdminScopeHelper::normalizeRecordDeptId($pool->dept_id ?? null);
|
||||
if (! AdminScopeHelper::isTemplateDeptId($poolDeptId)) {
|
||||
if (AdminScopeHelper::normalizeRecordDeptId($record->dept_id ?? null) !== $poolDeptId) {
|
||||
$record->dept_id = $poolDeptId;
|
||||
$record->save();
|
||||
}
|
||||
return $poolDeptId;
|
||||
}
|
||||
}
|
||||
|
||||
return $fallbackDeptId;
|
||||
}
|
||||
|
||||
private function aggregate(array $row, array &$resultCounts, array &$tierCounts): void
|
||||
{
|
||||
$grid = (int) ($row['roll_number_for_count'] ?? $row['roll_number'] ?? 0);
|
||||
@@ -229,10 +467,14 @@ class WeightTestRunner
|
||||
}
|
||||
}
|
||||
|
||||
private function rowForInsert(array $row, int $rewardConfigRecordId): array
|
||||
private function rowForInsert(array $row, int $rewardConfigRecordId, int $deptId): array
|
||||
{
|
||||
$bindDeptId = ! AdminScopeHelper::isTemplateDeptId($this->runDeptId)
|
||||
? $this->runDeptId
|
||||
: $deptId;
|
||||
$out = [
|
||||
'reward_config_record_id' => $rewardConfigRecordId,
|
||||
'dept_id' => $bindDeptId,
|
||||
];
|
||||
$keys = [
|
||||
'admin_id', 'lottery_config_id', 'lottery_type', 'is_win', 'win_coin',
|
||||
@@ -245,6 +487,9 @@ class WeightTestRunner
|
||||
$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;
|
||||
}
|
||||
|
||||
@@ -254,7 +499,7 @@ class WeightTestRunner
|
||||
return;
|
||||
}
|
||||
$this->insertBuffer($buffer);
|
||||
$buffer = [];
|
||||
array_splice($buffer, 0, count($buffer));
|
||||
$this->updateProgress($recordId, $done, $resultCounts, $tierCounts, $recordTotalPlayCount);
|
||||
}
|
||||
|
||||
@@ -263,6 +508,7 @@ class WeightTestRunner
|
||||
if (empty($rows)) {
|
||||
return;
|
||||
}
|
||||
$table = (new DicePlayRecordTest())->getTable();
|
||||
foreach ($rows as $row) {
|
||||
if (!is_array($row)) {
|
||||
continue;
|
||||
@@ -273,6 +519,9 @@ class WeightTestRunner
|
||||
$payload[$column] = $row[$column];
|
||||
}
|
||||
}
|
||||
if (! AdminScopeHelper::isTemplateDeptId($this->runDeptId)) {
|
||||
$payload['dept_id'] = $this->runDeptId;
|
||||
}
|
||||
if (!array_key_exists('create_time', $payload) || $payload['create_time'] === null || $payload['create_time'] === '') {
|
||||
$payload['create_time'] = date('Y-m-d H:i:s');
|
||||
}
|
||||
@@ -282,10 +531,26 @@ class WeightTestRunner
|
||||
if ($payload === []) {
|
||||
continue;
|
||||
}
|
||||
Db::name((new DicePlayRecordTest())->getTable())->insert($payload);
|
||||
// strict(false):表结构新增 dept_id 后,避免连接层字段缓存未刷新导致插入被丢弃
|
||||
Db::table($table)->strict(false)->insert($payload);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将本批测试明细 dept_id 与主记录对齐(修复历史 worker 未写入 dept_id 的情况)
|
||||
*/
|
||||
private function syncPlayRecordTestDeptId(int $recordId, int $deptId): void
|
||||
{
|
||||
if (AdminScopeHelper::isTemplateDeptId($deptId)) {
|
||||
return;
|
||||
}
|
||||
DicePlayRecordTest::where('reward_config_record_id', $recordId)
|
||||
->where(function ($query) {
|
||||
$query->whereNull('dept_id')->whereOr('dept_id', AdminScopeHelper::DEFAULT_TEMPLATE_DEPT);
|
||||
})
|
||||
->update(['dept_id' => $deptId]);
|
||||
}
|
||||
|
||||
private function updateProgress(int $recordId, int $overPlayCount, array $resultCounts, array $tierCounts, ?int $totalPlayCount = null): void
|
||||
{
|
||||
$record = DiceRewardConfigRecord::find($recordId);
|
||||
@@ -308,6 +573,13 @@ class WeightTestRunner
|
||||
{
|
||||
$record = DiceRewardConfigRecord::find($recordId);
|
||||
if ($record) {
|
||||
$deptId = AdminScopeHelper::normalizeRecordDeptId($record->dept_id ?? null);
|
||||
if (AdminScopeHelper::isTemplateDeptId($deptId) && ! AdminScopeHelper::isTemplateDeptId($this->runDeptId)) {
|
||||
$deptId = $this->runDeptId;
|
||||
$record->dept_id = $deptId;
|
||||
}
|
||||
$this->syncPlayRecordTestDeptId($recordId, $deptId);
|
||||
|
||||
// 平台盈利通过关联测试记录统计
|
||||
$platformProfit = DiceRewardConfigRecord::computePlatformProfitFromRelated($recordId);
|
||||
// 落点统计也通过关联测试记录重新统计,避免模拟过程异常导致为空
|
||||
|
||||
27
server/app/dice/model/DiceModel.php
Normal file
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\dice\model;
|
||||
|
||||
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
|
||||
{
|
||||
/**
|
||||
* @param mixed $data
|
||||
*/
|
||||
public static function destroy($data, bool $force = true): bool
|
||||
{
|
||||
return parent::destroy($data, true);
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,7 @@
|
||||
// +----------------------------------------------------------------------
|
||||
namespace app\dice\model\ante_config;
|
||||
|
||||
use plugin\saiadmin\basic\think\BaseModel;
|
||||
use app\dice\model\DiceModel;
|
||||
|
||||
/**
|
||||
* 底注配置模型
|
||||
@@ -19,7 +19,7 @@ use plugin\saiadmin\basic\think\BaseModel;
|
||||
* @property string $create_time 创建时间
|
||||
* @property string $update_time 更新时间
|
||||
*/
|
||||
class DiceAnteConfig extends BaseModel
|
||||
class DiceAnteConfig extends DiceModel
|
||||
{
|
||||
protected $pk = 'id';
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
// +----------------------------------------------------------------------
|
||||
namespace app\dice\model\config;
|
||||
|
||||
use plugin\saiadmin\basic\eloquent\BaseModel;
|
||||
use app\dice\model\DiceModel;
|
||||
|
||||
/**
|
||||
* 摇色子配置模型
|
||||
@@ -23,7 +23,7 @@ use plugin\saiadmin\basic\eloquent\BaseModel;
|
||||
* @property $create_time 创建时间
|
||||
* @property $update_time 修改时间
|
||||
*/
|
||||
class DiceConfig extends BaseModel
|
||||
class DiceConfig extends DiceModel
|
||||
{
|
||||
/**
|
||||
* 数据表主键
|
||||
|
||||
@@ -4,14 +4,14 @@
|
||||
// +----------------------------------------------------------------------
|
||||
namespace app\dice\model\game;
|
||||
|
||||
use plugin\saiadmin\basic\eloquent\BaseModel;
|
||||
use app\dice\model\DiceModel;
|
||||
|
||||
/**
|
||||
* 游戏管理模型
|
||||
*
|
||||
* dice_game 游戏配置表
|
||||
*/
|
||||
class DiceGame extends BaseModel
|
||||
class DiceGame extends DiceModel
|
||||
{
|
||||
protected $primaryKey = 'id';
|
||||
|
||||
|
||||
@@ -6,7 +6,9 @@
|
||||
// +----------------------------------------------------------------------
|
||||
namespace app\dice\model\lottery_pool_config;
|
||||
|
||||
use plugin\saiadmin\basic\think\BaseModel;
|
||||
use app\dice\helper\AdminScopeHelper;
|
||||
use app\dice\helper\ConfigScopeEditHelper;
|
||||
use app\dice\model\DiceModel;
|
||||
|
||||
/**
|
||||
* 色子奖池配置模型
|
||||
@@ -15,7 +17,8 @@ use plugin\saiadmin\basic\think\BaseModel;
|
||||
*
|
||||
* @property $id ID
|
||||
* @property $name 名称
|
||||
* @property $remark 备注
|
||||
* @property $remark 奖池名称(后台展示名)
|
||||
* @property $config_note 配置备注
|
||||
* @property $safety_line 安全线
|
||||
* @property $kill_enabled 是否启用杀分:0=关闭 1=开启
|
||||
* @property $create_time 创建时间
|
||||
@@ -27,8 +30,11 @@ use plugin\saiadmin\basic\think\BaseModel;
|
||||
* @property $t5_weight T5池权重
|
||||
* @property $profit_amount 池子累计盈利(每局付费按 win_coin-paid_amount,免费按 win_coin 累加;仅展示不可编辑)
|
||||
*/
|
||||
class DiceLotteryPoolConfig extends BaseModel
|
||||
class DiceLotteryPoolConfig extends DiceModel
|
||||
{
|
||||
/** 玩家默认彩金池(新玩家关联;付费未杀分时运行时读取该池 T1–T5 权重) */
|
||||
public const NAME_PLAYER_DEFAULT = 'playerDefault';
|
||||
|
||||
/**
|
||||
* 数据表主键
|
||||
* @var string
|
||||
@@ -41,12 +47,81 @@ class DiceLotteryPoolConfig extends BaseModel
|
||||
*/
|
||||
protected $table = 'dice_lottery_pool_config';
|
||||
|
||||
/** 列表/关联 JSON 附带奖池展示名 */
|
||||
protected $append = ['display_name'];
|
||||
|
||||
/**
|
||||
* 按名称与渠道查找奖池配置(一键测试等场景,避免命中其他渠道同名配置)
|
||||
*/
|
||||
public static function findByNameForDept(string $name, int $deptId): ?self
|
||||
{
|
||||
$query = (new self())->where('name', $name);
|
||||
ConfigScopeEditHelper::applyDeptIdWhere($query, AdminScopeHelper::normalizeRecordDeptId($deptId));
|
||||
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)
|
||||
{
|
||||
$query->where('name', 'like', '%'.$value.'%');
|
||||
$like = '%' . $value . '%';
|
||||
$query->where(function ($q) use ($like) {
|
||||
$q->where('name', 'like', $like)
|
||||
->whereOr('remark', 'like', $like);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ namespace app\dice\model\play_record;
|
||||
use app\dice\model\lottery_pool_config\DiceLotteryPoolConfig;
|
||||
use app\dice\model\player\DicePlayer;
|
||||
use app\dice\model\reward\DiceRewardConfig;
|
||||
use plugin\saiadmin\basic\think\BaseModel;
|
||||
use app\dice\model\DiceModel;
|
||||
use think\model\relation\BelongsTo;
|
||||
|
||||
/**
|
||||
@@ -31,6 +31,7 @@ use think\model\relation\BelongsTo;
|
||||
* @property $use_coins 消耗平台币(兼容字段:付费局=paid_amount,免费局=0)
|
||||
* @property $direction 方向:0=顺时针,1=逆时针
|
||||
* @property $reward_tier 中奖档位:T1,T2,T3,T4,T5,BIGWIN
|
||||
* @property $remark 备注(如惩罚格余额不足)
|
||||
* @property $lottery_id 奖池
|
||||
* @property $start_index 起始索引
|
||||
* @property $target_index 结束索引
|
||||
@@ -41,7 +42,7 @@ use think\model\relation\BelongsTo;
|
||||
* @property $create_time 创建时间
|
||||
* @property $update_time 修改时间
|
||||
*/
|
||||
class DicePlayRecord extends BaseModel
|
||||
class DicePlayRecord extends DiceModel
|
||||
{
|
||||
/**
|
||||
* 数据表主键
|
||||
@@ -87,13 +88,17 @@ class DicePlayRecord extends BaseModel
|
||||
}
|
||||
}
|
||||
|
||||
/** 按彩金池配置名称模糊(diceLotteryPoolConfig.name) */
|
||||
/** 按彩金池奖池名称或内部标识模糊搜索 */
|
||||
public function searchLotteryConfigNameAttr($query, $value)
|
||||
{
|
||||
if ($value === '' || $value === null) {
|
||||
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)) {
|
||||
$query->whereIn('lottery_config_id', $ids);
|
||||
} else {
|
||||
@@ -281,4 +286,20 @@ class DicePlayRecord extends BaseModel
|
||||
$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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
// +----------------------------------------------------------------------
|
||||
namespace app\dice\model\play_record_test;
|
||||
|
||||
use plugin\saiadmin\basic\think\BaseModel;
|
||||
use app\dice\model\DiceModel;
|
||||
use app\dice\model\reward_config_record\DiceRewardConfigRecord;
|
||||
use app\dice\model\lottery_pool_config\DiceLotteryPoolConfig;
|
||||
use think\model\relation\BelongsTo;
|
||||
@@ -37,7 +37,7 @@ use think\model\relation\BelongsTo;
|
||||
* @property $admin_id 所属管理员
|
||||
* @property int|null $reward_config_record_id 关联 DiceRewardConfigRecord.id(权重测试记录)
|
||||
*/
|
||||
class DicePlayRecordTest extends BaseModel
|
||||
class DicePlayRecordTest extends DiceModel
|
||||
{
|
||||
/**
|
||||
* 数据表主键
|
||||
@@ -73,6 +73,15 @@ class DicePlayRecordTest extends BaseModel
|
||||
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=免费 */
|
||||
public function searchLotteryTypeAttr($query, $value)
|
||||
{
|
||||
|
||||
@@ -6,7 +6,8 @@
|
||||
// +----------------------------------------------------------------------
|
||||
namespace app\dice\model\player;
|
||||
|
||||
use plugin\saiadmin\basic\think\BaseModel;
|
||||
use app\dice\helper\AdminScopeHelper;
|
||||
use app\dice\model\DiceModel;
|
||||
use app\dice\model\lottery_pool_config\DiceLotteryPoolConfig;
|
||||
|
||||
/**
|
||||
@@ -15,7 +16,8 @@ use app\dice\model\lottery_pool_config\DiceLotteryPoolConfig;
|
||||
* dice_player 大富翁-玩家
|
||||
*
|
||||
* @property $id ID
|
||||
* @property $username 用户名
|
||||
* @property $dept_id 所属渠道ID
|
||||
* @property $username 用户名(同渠道内唯一)
|
||||
* @property $phone 手机
|
||||
* @property $uid uid
|
||||
* @property $name 昵称
|
||||
@@ -37,7 +39,7 @@ use app\dice\model\lottery_pool_config\DiceLotteryPoolConfig;
|
||||
* @property $update_time 更新时间
|
||||
* @property $delete_time 删除时间
|
||||
*/
|
||||
class DicePlayer extends BaseModel
|
||||
class DicePlayer extends DiceModel
|
||||
{
|
||||
/**
|
||||
* 数据表主键
|
||||
@@ -83,26 +85,61 @@ class DicePlayer extends BaseModel
|
||||
if ($name === null || $name === '') {
|
||||
$model->setAttr('name', $uid);
|
||||
}
|
||||
// 创建玩家时:未指定则自动保存 lottery_config_id 为 DiceLotteryPoolConfig name=default 的 id,没有则为 0
|
||||
// 创建玩家时:未指定则关联 name=playerDefault(玩家默认彩金池),没有则为 0
|
||||
try {
|
||||
$lotteryConfigId = $model->getAttr('lottery_config_id');
|
||||
} catch (\Throwable $e) {
|
||||
$lotteryConfigId = null;
|
||||
}
|
||||
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);
|
||||
}
|
||||
// 彩金池权重默认取 name=default 的奖池配置
|
||||
// 展示用权重:从玩家关联的彩金池复制(playerDefault 在抽奖时仍实时读池配置)
|
||||
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
|
||||
{
|
||||
$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) {
|
||||
return;
|
||||
}
|
||||
@@ -187,6 +224,26 @@ class DicePlayer extends BaseModel
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册时间起始 搜索
|
||||
*/
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 关联彩金池配置
|
||||
*/
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
namespace app\dice\model\player_ticket_record;
|
||||
|
||||
use app\dice\model\player\DicePlayer;
|
||||
use plugin\saiadmin\basic\think\BaseModel;
|
||||
use app\dice\model\DiceModel;
|
||||
use think\model\relation\BelongsTo;
|
||||
|
||||
/**
|
||||
@@ -27,7 +27,7 @@ use think\model\relation\BelongsTo;
|
||||
* @property $create_time 创建时间
|
||||
* @property $update_time 修改时间
|
||||
*/
|
||||
class DicePlayerTicketRecord extends BaseModel
|
||||
class DicePlayerTicketRecord extends DiceModel
|
||||
{
|
||||
/**
|
||||
* 数据表主键
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
namespace app\dice\model\player_wallet_record;
|
||||
|
||||
use app\dice\model\player\DicePlayer;
|
||||
use plugin\saiadmin\basic\think\BaseModel;
|
||||
use app\dice\model\DiceModel;
|
||||
use plugin\saiadmin\app\model\system\SystemUser;
|
||||
use think\model\relation\BelongsTo;
|
||||
|
||||
@@ -31,7 +31,7 @@ use think\model\relation\BelongsTo;
|
||||
* @property $create_time 创建时间
|
||||
* @property $update_time 修改时间
|
||||
*/
|
||||
class DicePlayerWalletRecord extends BaseModel
|
||||
class DicePlayerWalletRecord extends DiceModel
|
||||
{
|
||||
/**
|
||||
* 数据表主键
|
||||
|
||||
@@ -4,7 +4,9 @@
|
||||
// +----------------------------------------------------------------------
|
||||
namespace app\dice\model\reward;
|
||||
|
||||
use plugin\saiadmin\basic\think\BaseModel;
|
||||
use app\dice\helper\AdminScopeHelper;
|
||||
use app\dice\helper\ConfigScopeEditHelper;
|
||||
use app\dice\model\DiceModel;
|
||||
use support\think\Cache;
|
||||
|
||||
/**
|
||||
@@ -25,42 +27,70 @@ use support\think\Cache;
|
||||
* @property $remark 备注(来自config)
|
||||
* @property $type 奖励类型(来自config)
|
||||
*/
|
||||
class DiceReward extends BaseModel
|
||||
class DiceReward extends DiceModel
|
||||
{
|
||||
/** 方向:顺时针 */
|
||||
public const DIRECTION_CLOCKWISE = 0;
|
||||
/** 方向:逆时针 */
|
||||
public const DIRECTION_COUNTERCLOCKWISE = 1;
|
||||
|
||||
/** 缓存键:奖励对照实例 */
|
||||
/** 缓存键前缀:奖励对照实例(按渠道分键) */
|
||||
private const CACHE_KEY_INSTANCE = 'dice:reward:instance';
|
||||
|
||||
private const CACHE_TTL = 86400 * 30;
|
||||
|
||||
private static ?array $instance = null;
|
||||
|
||||
private static ?int $requestDeptId = null;
|
||||
|
||||
protected $table = 'dice_reward';
|
||||
|
||||
/** 主键 id 自增,唯一约束 (direction, grid_number) */
|
||||
protected $pk = 'id';
|
||||
|
||||
private static function cacheKeyForDept(int $deptId): string
|
||||
{
|
||||
return self::CACHE_KEY_INSTANCE . ':' . $deptId;
|
||||
}
|
||||
|
||||
private static function resolveDeptId(?int $deptId): int
|
||||
{
|
||||
if ($deptId !== null) {
|
||||
return AdminScopeHelper::normalizeRecordDeptId($deptId);
|
||||
}
|
||||
if (self::$requestDeptId !== null) {
|
||||
return self::$requestDeptId;
|
||||
}
|
||||
return AdminScopeHelper::DEFAULT_TEMPLATE_DEPT;
|
||||
}
|
||||
|
||||
/**
|
||||
* 请求级设置当前渠道(一键测试 worker 内调用 simulateOnePlay 前设置)
|
||||
*/
|
||||
public static function setRequestDeptId(?int $deptId): void
|
||||
{
|
||||
self::$requestDeptId = $deptId !== null
|
||||
? AdminScopeHelper::normalizeRecordDeptId($deptId)
|
||||
: null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取奖励对照实例(按档位+方向索引,用于抽奖与权重配比)
|
||||
* 优先从共享缓存读取,保证多进程(如一键测试 worker)与数据库一致
|
||||
* @return array{list: array, by_tier_direction: array}
|
||||
*/
|
||||
public static function getCachedInstance(): array
|
||||
public static function getCachedInstance(?int $deptId = null): array
|
||||
{
|
||||
$instance = Cache::get(self::CACHE_KEY_INSTANCE);
|
||||
$deptId = self::resolveDeptId($deptId);
|
||||
$cacheKey = self::cacheKeyForDept($deptId);
|
||||
$instance = Cache::get($cacheKey);
|
||||
if ($instance !== null && is_array($instance)) {
|
||||
self::$instance = $instance;
|
||||
return $instance;
|
||||
}
|
||||
if (self::$instance !== null) {
|
||||
if (self::$instance !== null && self::$requestDeptId === $deptId) {
|
||||
return self::$instance;
|
||||
}
|
||||
self::refreshCache();
|
||||
$instance = Cache::get(self::CACHE_KEY_INSTANCE);
|
||||
self::refreshCache($deptId);
|
||||
$instance = Cache::get($cacheKey);
|
||||
self::$instance = is_array($instance) ? $instance : self::buildEmptyInstance();
|
||||
return self::$instance;
|
||||
}
|
||||
@@ -69,9 +99,9 @@ class DiceReward extends BaseModel
|
||||
* 按档位+方向取权重列表(用于抽奖:该档位该方向下 end_index => weight)
|
||||
* @return array<int, int> end_index => weight
|
||||
*/
|
||||
public static function getCachedByTierAndDirection(string $tier, int $direction): array
|
||||
public static function getCachedByTierAndDirection(string $tier, int $direction, ?int $deptId = null): array
|
||||
{
|
||||
$inst = self::getCachedInstance();
|
||||
$inst = self::getCachedInstance($deptId);
|
||||
$byTierDirection = $inst['by_tier_direction'] ?? [];
|
||||
$list = $byTierDirection[$tier][$direction] ?? [];
|
||||
$result = [];
|
||||
@@ -84,11 +114,14 @@ class DiceReward extends BaseModel
|
||||
}
|
||||
|
||||
/**
|
||||
* 重新从数据库加载并写入缓存;修改/新增/删除后需调用以实例化
|
||||
* 按渠道从数据库加载并写入缓存
|
||||
*/
|
||||
public static function refreshCache(): void
|
||||
public static function refreshCache(?int $deptId = null): void
|
||||
{
|
||||
$list = (new self())->order('tier')->order('direction')->order('end_index')->select()->toArray();
|
||||
$deptId = self::resolveDeptId($deptId);
|
||||
$query = (new self())->order('tier')->order('direction')->order('end_index');
|
||||
ConfigScopeEditHelper::applyDeptIdWhere($query, $deptId);
|
||||
$list = $query->select()->toArray();
|
||||
$byTierDirection = [];
|
||||
foreach ($list as $row) {
|
||||
$tier = isset($row['tier']) ? (string) $row['tier'] : '';
|
||||
@@ -103,11 +136,12 @@ class DiceReward extends BaseModel
|
||||
$byTierDirection[$tier][$direction][] = $row;
|
||||
}
|
||||
}
|
||||
self::$instance = [
|
||||
$instance = [
|
||||
'list' => $list,
|
||||
'by_tier_direction' => $byTierDirection,
|
||||
];
|
||||
Cache::set(self::CACHE_KEY_INSTANCE, self::$instance, self::CACHE_TTL);
|
||||
self::$instance = $instance;
|
||||
Cache::set(self::cacheKeyForDept($deptId), $instance, self::CACHE_TTL);
|
||||
}
|
||||
|
||||
private static function buildEmptyInstance(): array
|
||||
@@ -121,20 +155,29 @@ class DiceReward extends BaseModel
|
||||
public static function clearRequestInstance(): void
|
||||
{
|
||||
self::$instance = null;
|
||||
self::$requestDeptId = null;
|
||||
}
|
||||
|
||||
private static function refreshCacheForModel($model): void
|
||||
{
|
||||
$deptId = AdminScopeHelper::normalizeRecordDeptId(
|
||||
is_array($model) ? ($model['dept_id'] ?? null) : ($model->dept_id ?? null)
|
||||
);
|
||||
self::refreshCache($deptId);
|
||||
}
|
||||
|
||||
public static function onAfterInsert($model): void
|
||||
{
|
||||
self::refreshCache();
|
||||
self::refreshCacheForModel($model);
|
||||
}
|
||||
|
||||
public static function onAfterUpdate($model): void
|
||||
{
|
||||
self::refreshCache();
|
||||
self::refreshCacheForModel($model);
|
||||
}
|
||||
|
||||
public static function onAfterDelete($model): void
|
||||
{
|
||||
self::refreshCache();
|
||||
self::refreshCacheForModel($model);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,8 +6,10 @@
|
||||
// +----------------------------------------------------------------------
|
||||
namespace app\dice\model\reward_config;
|
||||
|
||||
use app\dice\helper\AdminScopeHelper;
|
||||
use app\dice\helper\ConfigScopeEditHelper;
|
||||
use app\dice\model\reward\DiceReward;
|
||||
use plugin\saiadmin\basic\think\BaseModel;
|
||||
use app\dice\model\DiceModel;
|
||||
use support\think\Cache;
|
||||
|
||||
/**
|
||||
@@ -27,7 +29,7 @@ use support\think\Cache;
|
||||
* @property $create_time 创建时间
|
||||
* @property $update_time 修改时间
|
||||
*/
|
||||
class DiceRewardConfig extends BaseModel
|
||||
class DiceRewardConfig extends DiceModel
|
||||
{
|
||||
/** 缓存键:彩金池奖励列表实例 */
|
||||
private const CACHE_KEY_INSTANCE = 'dice:reward_config:instance';
|
||||
@@ -45,31 +47,35 @@ class DiceRewardConfig extends BaseModel
|
||||
* 优先从共享缓存读取,保证多进程(如一键测试 worker)能拿到最新配置,与数据库一致
|
||||
* @return array{list: array, by_tier: array, by_tier_grid: array, min_real_ev: float}
|
||||
*/
|
||||
public static function getCachedInstance(): array
|
||||
public static function getCachedInstance(?int $deptId = null): array
|
||||
{
|
||||
$instance = Cache::get(self::CACHE_KEY_INSTANCE);
|
||||
if ($deptId === null) {
|
||||
$deptId = AdminScopeHelper::DEFAULT_TEMPLATE_DEPT;
|
||||
}
|
||||
$cacheKey = self::cacheKeyForDept($deptId);
|
||||
$instance = Cache::get($cacheKey);
|
||||
if ($instance !== null && is_array($instance)) {
|
||||
self::$instance = $instance;
|
||||
return $instance;
|
||||
}
|
||||
if (self::$instance !== null) {
|
||||
return self::$instance;
|
||||
}
|
||||
self::refreshCache();
|
||||
$instance = Cache::get(self::CACHE_KEY_INSTANCE);
|
||||
self::$instance = is_array($instance) ? $instance : self::buildEmptyInstance();
|
||||
return self::$instance;
|
||||
self::refreshCache($deptId);
|
||||
$instance = Cache::get($cacheKey);
|
||||
return is_array($instance) ? $instance : self::buildEmptyInstance();
|
||||
}
|
||||
|
||||
public static function getCachedList(): array
|
||||
private static function cacheKeyForDept(int $deptId): string
|
||||
{
|
||||
$inst = self::getCachedInstance();
|
||||
return self::CACHE_KEY_INSTANCE . ':' . $deptId;
|
||||
}
|
||||
|
||||
public static function getCachedList(?int $deptId = null): array
|
||||
{
|
||||
$inst = self::getCachedInstance($deptId);
|
||||
return $inst['list'] ?? [];
|
||||
}
|
||||
|
||||
public static function getCachedById(int $id): ?array
|
||||
public static function getCachedById(int $id, ?int $deptId = null): ?array
|
||||
{
|
||||
$list = self::getCachedList();
|
||||
$list = self::getCachedList($deptId);
|
||||
foreach ($list as $row) {
|
||||
if (isset($row['id']) && (int) $row['id'] === $id) {
|
||||
return $row;
|
||||
@@ -79,11 +85,16 @@ class DiceRewardConfig extends BaseModel
|
||||
}
|
||||
|
||||
/**
|
||||
* 重新从数据库加载并写入缓存(按档位+权重抽 grid_number,含 by_tier、by_tier_grid)
|
||||
* 按渠道从数据库加载并写入缓存(避免多渠道配置混在同一缓存键)
|
||||
*/
|
||||
public static function refreshCache(): void
|
||||
public static function refreshCache(?int $deptId = null): void
|
||||
{
|
||||
$list = (new self())->order('id', 'asc')->select()->toArray();
|
||||
if ($deptId === null) {
|
||||
$deptId = AdminScopeHelper::DEFAULT_TEMPLATE_DEPT;
|
||||
}
|
||||
$query = (new self())->order('id', 'asc');
|
||||
ConfigScopeEditHelper::applyDeptIdWhere($query, $deptId);
|
||||
$list = $query->select()->toArray();
|
||||
$byTier = [];
|
||||
$byTierGrid = [];
|
||||
foreach ($list as $row) {
|
||||
@@ -103,13 +114,16 @@ class DiceRewardConfig extends BaseModel
|
||||
}
|
||||
}
|
||||
$minRealEv = empty($list) ? 0.0 : (float) min(array_column($list, 'real_ev'));
|
||||
self::$instance = [
|
||||
$instance = [
|
||||
'list' => $list,
|
||||
'by_tier' => $byTier,
|
||||
'by_tier_grid' => $byTierGrid,
|
||||
'min_real_ev' => $minRealEv,
|
||||
];
|
||||
Cache::set(self::CACHE_KEY_INSTANCE, self::$instance, self::CACHE_TTL);
|
||||
Cache::set(self::cacheKeyForDept($deptId), $instance, self::CACHE_TTL);
|
||||
if ($deptId === AdminScopeHelper::DEFAULT_TEMPLATE_DEPT) {
|
||||
self::$instance = $instance;
|
||||
}
|
||||
}
|
||||
|
||||
private static function buildEmptyInstance(): array
|
||||
@@ -125,9 +139,9 @@ class DiceRewardConfig extends BaseModel
|
||||
/**
|
||||
* 按档位+色子点数取一条(用于 BIGWIN)
|
||||
*/
|
||||
public static function getCachedByTierAndGridNumber(string $tier, int $gridNumber): ?array
|
||||
public static function getCachedByTierAndGridNumber(string $tier, int $gridNumber, ?int $deptId = null): ?array
|
||||
{
|
||||
$inst = self::getCachedInstance();
|
||||
$inst = self::getCachedInstance($deptId);
|
||||
$byTierGrid = $inst['by_tier_grid'] ?? [];
|
||||
$tierData = $byTierGrid[$tier] ?? [];
|
||||
$row = $tierData[$gridNumber] ?? null;
|
||||
@@ -143,9 +157,9 @@ class DiceRewardConfig extends BaseModel
|
||||
/**
|
||||
* 从缓存按档位取奖励列表(不含权重,仅配置)
|
||||
*/
|
||||
public static function getCachedByTier(string $tier): array
|
||||
public static function getCachedByTier(string $tier, ?int $deptId = null): array
|
||||
{
|
||||
$inst = self::getCachedInstance();
|
||||
$inst = self::getCachedInstance($deptId);
|
||||
$byTier = $inst['by_tier'] ?? [];
|
||||
return $byTier[$tier] ?? [];
|
||||
}
|
||||
@@ -155,10 +169,10 @@ class DiceRewardConfig extends BaseModel
|
||||
* @param int $direction 0=顺时针, 1=逆时针
|
||||
* @return array 每行含 id, grid_number, real_ev, tier, weight 等
|
||||
*/
|
||||
public static function getCachedByTierForDirection(string $tier, int $direction): array
|
||||
public static function getCachedByTierForDirection(string $tier, int $direction, ?int $deptId = null): array
|
||||
{
|
||||
$list = self::getCachedByTier($tier);
|
||||
$weightMap = DiceReward::getCachedByTierAndDirection($tier, $direction);
|
||||
$list = self::getCachedByTier($tier, $deptId);
|
||||
$weightMap = DiceReward::getCachedByTierAndDirection($tier, $direction, $deptId);
|
||||
foreach ($list as $i => $row) {
|
||||
$id = isset($row['id']) ? (int) $row['id'] : 0;
|
||||
$list[$i]['weight'] = $weightMap[$id] ?? 1;
|
||||
@@ -171,19 +185,27 @@ class DiceRewardConfig extends BaseModel
|
||||
self::$instance = null;
|
||||
}
|
||||
|
||||
private static function refreshCacheForModel($model): void
|
||||
{
|
||||
$deptId = AdminScopeHelper::normalizeRecordDeptId(
|
||||
is_array($model) ? ($model['dept_id'] ?? null) : ($model->dept_id ?? null)
|
||||
);
|
||||
self::refreshCache($deptId);
|
||||
}
|
||||
|
||||
public static function onAfterInsert($model): void
|
||||
{
|
||||
self::refreshCache();
|
||||
self::refreshCacheForModel($model);
|
||||
}
|
||||
|
||||
public static function onAfterUpdate($model): void
|
||||
{
|
||||
self::refreshCache();
|
||||
self::refreshCacheForModel($model);
|
||||
}
|
||||
|
||||
public static function onAfterDelete($model): void
|
||||
{
|
||||
self::refreshCache();
|
||||
self::refreshCacheForModel($model);
|
||||
}
|
||||
|
||||
public function searchGridNumberMinAttr($query, $value)
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
// +----------------------------------------------------------------------
|
||||
namespace app\dice\model\reward_config;
|
||||
|
||||
use plugin\saiadmin\basic\think\BaseModel;
|
||||
use app\dice\model\DiceModel;
|
||||
|
||||
/**
|
||||
* 权重配比测试记录模型
|
||||
@@ -20,7 +20,7 @@ use plugin\saiadmin\basic\think\BaseModel;
|
||||
* @property int|null $admin_id 执行测试的管理员ID
|
||||
* @property string|null $create_time 创建时间
|
||||
*/
|
||||
class DiceRewardConfigRecord extends BaseModel
|
||||
class DiceRewardConfigRecord extends DiceModel
|
||||
{
|
||||
protected $pk = 'id';
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
namespace app\dice\model\reward_config_record;
|
||||
|
||||
use app\dice\model\play_record_test\DicePlayRecordTest;
|
||||
use plugin\saiadmin\basic\think\BaseModel;
|
||||
use app\dice\model\DiceModel;
|
||||
use think\model\relation\HasMany;
|
||||
|
||||
/**
|
||||
@@ -43,7 +43,7 @@ use think\model\relation\HasMany;
|
||||
* @property int|null $admin_id 执行测试的管理员ID
|
||||
* @property string|null $create_time 创建时间
|
||||
*/
|
||||
class DiceRewardConfigRecord extends BaseModel
|
||||
class DiceRewardConfigRecord extends DiceModel
|
||||
{
|
||||
/** 状态:失败 */
|
||||
public const STATUS_FAIL = -1;
|
||||
|
||||
640
server/app/dice/service/DiceChannelConfigService.php
Normal file
@@ -0,0 +1,640 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\dice\service;
|
||||
|
||||
use app\dice\helper\AdminScopeHelper;
|
||||
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_config\DiceRewardConfig;
|
||||
use plugin\saiadmin\app\model\system\SystemDept;
|
||||
use plugin\saiadmin\app\model\system\SystemUser;
|
||||
use plugin\saiadmin\exception\ApiException;
|
||||
use support\think\Db;
|
||||
|
||||
/**
|
||||
* 渠道默认配置复制、补齐与关联删除
|
||||
* 默认配置:dept_id = 0(与超管「默认配置模板」一致)
|
||||
*/
|
||||
class DiceChannelConfigService
|
||||
{
|
||||
/** 需 (dept_id, id) 复合唯一的配置表 */
|
||||
private const COMPOSITE_KEY_TABLES = [
|
||||
'dice_config',
|
||||
'dice_reward_config',
|
||||
];
|
||||
/** 从默认模板复制的配置表 */
|
||||
private const CONFIG_TABLES = [
|
||||
'dice_config',
|
||||
'dice_ante_config',
|
||||
'dice_lottery_pool_config',
|
||||
'dice_reward_config',
|
||||
'dice_game',
|
||||
];
|
||||
|
||||
/** 复制时必须保留主键 id(非自增或固定 0-25) */
|
||||
private const TABLES_KEEP_ID = [
|
||||
'dice_config',
|
||||
'dice_reward_config',
|
||||
];
|
||||
|
||||
/** 可关联删除的业务表 */
|
||||
private const RELATION_TABLES = [
|
||||
'dice_config' => ['label' => '游戏键值配置', 'group' => 'configs'],
|
||||
'dice_ante_config' => ['label' => '底注配置', 'group' => 'configs'],
|
||||
'dice_lottery_pool_config' => ['label' => '彩金池配置', 'group' => 'configs'],
|
||||
'dice_reward_config' => ['label' => '奖励索引配置', 'group' => 'configs'],
|
||||
'dice_reward' => ['label' => '中奖概率(奖励对照)', 'group' => 'configs'],
|
||||
'dice_game' => ['label' => '游戏管理', 'group' => 'configs'],
|
||||
'dice_player' => ['label' => '玩家', 'group' => 'players'],
|
||||
'dice_play_record' => ['label' => '抽奖记录', 'group' => 'records'],
|
||||
'dice_play_record_test' => ['label' => '测试抽奖记录', 'group' => 'records'],
|
||||
'dice_player_wallet_record' => ['label' => '钱包流水', 'group' => 'records'],
|
||||
'dice_player_ticket_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) 唯一约束
|
||||
*/
|
||||
public function ensureConfigCompositeKeys(): void
|
||||
{
|
||||
foreach (array_merge(self::CONFIG_TABLES, ['dice_reward']) as $table) {
|
||||
if ($this->tableHasColumn($table, 'dept_id')) {
|
||||
Db::table($table)->whereNull('dept_id')->update(['dept_id' => AdminScopeHelper::DEFAULT_TEMPLATE_DEPT]);
|
||||
}
|
||||
}
|
||||
foreach (self::COMPOSITE_KEY_TABLES as $table) {
|
||||
if (!$this->tableHasColumn($table, 'dept_id')) {
|
||||
continue;
|
||||
}
|
||||
if (!$this->tableHasColumn($table, 'row_id')) {
|
||||
if ($table === 'dice_reward_config') {
|
||||
Db::execute(
|
||||
'ALTER TABLE `dice_reward_config`'
|
||||
. ' MODIFY `id` int(11) NOT NULL COMMENT \'ID\','
|
||||
. ' ADD COLUMN `row_id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT FIRST,'
|
||||
. ' DROP PRIMARY KEY,'
|
||||
. ' ADD PRIMARY KEY (`row_id`)'
|
||||
);
|
||||
} else {
|
||||
Db::execute(
|
||||
'ALTER TABLE `dice_config`'
|
||||
. ' ADD COLUMN `row_id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT FIRST,'
|
||||
. ' DROP PRIMARY KEY,'
|
||||
. ' ADD PRIMARY KEY (`row_id`)'
|
||||
);
|
||||
}
|
||||
}
|
||||
$indexes = Db::query("SHOW INDEX FROM `{$table}` WHERE Key_name = 'uk_dept_config'");
|
||||
if (empty($indexes)) {
|
||||
Db::execute("ALTER TABLE `{$table}` ADD UNIQUE KEY `uk_dept_config` (`dept_id`, `id`)");
|
||||
}
|
||||
}
|
||||
$this->ensureDeptScopedUniqueIndexes();
|
||||
}
|
||||
|
||||
/**
|
||||
* 将全局唯一键改为按渠道 (dept_id, 业务键) 唯一,便于复制默认模板
|
||||
*/
|
||||
private function ensureDeptScopedUniqueIndexes(): void
|
||||
{
|
||||
if ($this->tableHasColumn('dice_lottery_pool_config', 'dept_id')) {
|
||||
$old = Db::query("SHOW INDEX FROM `dice_lottery_pool_config` WHERE Key_name = 'dice_lottery_poll_config_unique'");
|
||||
if (!empty($old)) {
|
||||
Db::execute('ALTER TABLE `dice_lottery_pool_config` DROP INDEX `dice_lottery_poll_config_unique`');
|
||||
}
|
||||
$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`)');
|
||||
}
|
||||
}
|
||||
if ($this->tableHasColumn('dice_game', 'dept_id')) {
|
||||
foreach (['uk_dice_game_code', 'uk_dice_game_key'] as $idx) {
|
||||
$exists = Db::query("SHOW INDEX FROM `dice_game` WHERE Key_name = '{$idx}'");
|
||||
if (!empty($exists)) {
|
||||
Db::execute("ALTER TABLE `dice_game` DROP INDEX `{$idx}`");
|
||||
}
|
||||
}
|
||||
$ukCode = Db::query("SHOW INDEX FROM `dice_game` WHERE Key_name = 'uk_dept_game_code'");
|
||||
if (empty($ukCode)) {
|
||||
Db::execute('ALTER TABLE `dice_game` ADD UNIQUE KEY `uk_dept_game_code` (`dept_id`, `game_code`)');
|
||||
}
|
||||
$ukKey = Db::query("SHOW INDEX FROM `dice_game` WHERE Key_name = 'uk_dept_game_key'");
|
||||
if (empty($ukKey)) {
|
||||
Db::execute('ALTER TABLE `dice_game` ADD UNIQUE KEY `uk_dept_game_key` (`dept_id`, `game_key`)');
|
||||
}
|
||||
}
|
||||
if ($this->tableHasColumn('dice_reward', 'dept_id')) {
|
||||
$old = Db::query("SHOW INDEX FROM `dice_reward` WHERE Key_name = 'uk_direction_grid_number'");
|
||||
if (!empty($old)) {
|
||||
Db::execute('ALTER TABLE `dice_reward` DROP INDEX `uk_direction_grid_number`');
|
||||
}
|
||||
$uk = Db::query("SHOW INDEX FROM `dice_reward` WHERE Key_name = 'uk_dept_direction_grid'");
|
||||
if (empty($uk)) {
|
||||
Db::execute('ALTER TABLE `dice_reward` ADD UNIQUE KEY `uk_dept_direction_grid` (`dept_id`, `direction`, `grid_number`)');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将当前无 dept_id 的配置标记为默认模板(仅执行一次迁移)
|
||||
*/
|
||||
public function markLegacyConfigAsDefault(): int
|
||||
{
|
||||
$this->ensureConfigCompositeKeys();
|
||||
$total = 0;
|
||||
foreach (self::CONFIG_TABLES as $table) {
|
||||
if (!$this->tableHasColumn($table, 'dept_id')) {
|
||||
continue;
|
||||
}
|
||||
$total += $this->countByDept($table, AdminScopeHelper::DEFAULT_TEMPLATE_DEPT);
|
||||
}
|
||||
return $total;
|
||||
}
|
||||
|
||||
/**
|
||||
* 为单个渠道从默认模板复制配置(已存在则跳过)
|
||||
*/
|
||||
public function copyDefaultConfigToDept(int $deptId): array
|
||||
{
|
||||
if ($deptId <= 0) {
|
||||
throw new ApiException('Invalid channel id');
|
||||
}
|
||||
$result = ['dept_id' => $deptId, 'copied' => [], 'skipped' => [], 'merged' => []];
|
||||
foreach (self::CONFIG_TABLES as $table) {
|
||||
if (!$this->tableHasColumn($table, 'dept_id')) {
|
||||
continue;
|
||||
}
|
||||
if (in_array($table, self::TABLES_KEEP_ID, true)) {
|
||||
$merged = $this->syncCompositeIdTableFromDefault($table, $deptId);
|
||||
if ($merged > 0) {
|
||||
$result['merged'][$table] = $merged;
|
||||
} elseif ($this->countByDept($table, $deptId) > 0) {
|
||||
$result['skipped'][] = $table;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if ($this->countByDept($table, $deptId) > 0) {
|
||||
$result['skipped'][] = $table;
|
||||
continue;
|
||||
}
|
||||
$rows = $this->defaultTemplateRows($table);
|
||||
if (empty($rows)) {
|
||||
continue;
|
||||
}
|
||||
foreach ($rows as $row) {
|
||||
$row = (array) $row;
|
||||
unset($row['id'], $row['row_id'], $row['create_time'], $row['update_time'], $row['delete_time']);
|
||||
$row['dept_id'] = $deptId;
|
||||
Db::table($table)->insert($row);
|
||||
}
|
||||
$result['copied'][] = $table;
|
||||
}
|
||||
$this->ensureRewardReferenceForDept($deptId);
|
||||
DiceRewardConfig::refreshCache($deptId);
|
||||
$this->ensurePlayerDefaultPoolForDept($deptId);
|
||||
$this->migratePlayersDefaultPoolToPlayerDefault($deptId);
|
||||
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)
|
||||
*/
|
||||
private function syncCompositeIdTableFromDefault(string $table, int $deptId): int
|
||||
{
|
||||
$templateRows = $this->defaultTemplateRows($table);
|
||||
if (empty($templateRows)) {
|
||||
return 0;
|
||||
}
|
||||
$inserted = 0;
|
||||
foreach ($templateRows as $row) {
|
||||
$row = (array) $row;
|
||||
if (!isset($row['id'])) {
|
||||
continue;
|
||||
}
|
||||
$businessId = $row['id'];
|
||||
$exists = Db::table($table)->where('dept_id', $deptId)->where('id', $businessId)->count();
|
||||
if ($exists > 0) {
|
||||
continue;
|
||||
}
|
||||
unset($row['row_id'], $row['create_time'], $row['update_time'], $row['delete_time']);
|
||||
$row['dept_id'] = $deptId;
|
||||
Db::table($table)->insert($row);
|
||||
$inserted++;
|
||||
}
|
||||
return $inserted;
|
||||
}
|
||||
|
||||
/**
|
||||
* 渠道已有奖励索引时,自动生成 dice_reward 对照表
|
||||
*/
|
||||
public function ensureRewardReferenceForDept(int $deptId): void
|
||||
{
|
||||
if ($deptId <= 0 || !$this->tableHasColumn('dice_reward', 'dept_id')) {
|
||||
return;
|
||||
}
|
||||
if ($this->countByDept('dice_reward_config', $deptId) <= 0) {
|
||||
return;
|
||||
}
|
||||
if ($this->countByDept('dice_reward', $deptId) > 0) {
|
||||
return;
|
||||
}
|
||||
$logic = new DiceRewardLogic();
|
||||
$logic->createRewardReferenceFromConfig($deptId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 复制默认 dice_reward 到渠道
|
||||
*/
|
||||
public function copyDefaultRewardsToDept(int $deptId): void
|
||||
{
|
||||
$this->ensureRewardReferenceForDept($deptId);
|
||||
if (!$this->tableHasColumn('dice_reward', 'dept_id')) {
|
||||
return;
|
||||
}
|
||||
if ($this->countByDept('dice_reward', $deptId) > 0) {
|
||||
return;
|
||||
}
|
||||
if ($this->countByDept('dice_reward_config', $deptId) > 0) {
|
||||
return;
|
||||
}
|
||||
$rows = $this->defaultTemplateRows('dice_reward');
|
||||
foreach ($rows as $row) {
|
||||
$row = (array) $row;
|
||||
unset($row['id'], $row['row_id']);
|
||||
unset($row['create_time'], $row['update_time'], $row['delete_time']);
|
||||
$row['dept_id'] = $deptId;
|
||||
Db::table('dice_reward')->insert($row);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 为所有已有渠道补齐缺失配置
|
||||
*/
|
||||
public function syncAllChannelsFromDefault(): array
|
||||
{
|
||||
$deptIds = SystemDept::column('id');
|
||||
$summary = [];
|
||||
foreach ($deptIds as $deptId) {
|
||||
$deptId = (int) $deptId;
|
||||
if ($deptId <= 0) {
|
||||
continue;
|
||||
}
|
||||
$summary[$deptId] = $this->copyDefaultConfigToDept($deptId);
|
||||
}
|
||||
$summary['_player_default_pools'] = $this->ensurePlayerDefaultPoolsAllChannels();
|
||||
return $summary;
|
||||
}
|
||||
|
||||
/**
|
||||
* 修复已删除渠道 ID、无管理员关联的遗留数据,归并到首个顶级渠道
|
||||
*/
|
||||
public function repairOrphanDeptReferences(): array
|
||||
{
|
||||
$validDeptIds = array_map('intval', SystemDept::column('id') ?: []);
|
||||
if (empty($validDeptIds)) {
|
||||
return [];
|
||||
}
|
||||
$rootDeptId = min($validDeptIds);
|
||||
$stats = [];
|
||||
$inList = implode(',', $validDeptIds);
|
||||
|
||||
$stats['sa_system_user'] = Db::execute(
|
||||
"UPDATE sa_system_user SET dept_id = {$rootDeptId}
|
||||
WHERE dept_id IS NOT NULL AND dept_id > 0 AND dept_id NOT IN ({$inList})"
|
||||
);
|
||||
|
||||
$bizTables = [
|
||||
'dice_player',
|
||||
'dice_play_record',
|
||||
'dice_play_record_test',
|
||||
'dice_player_wallet_record',
|
||||
'dice_player_ticket_record',
|
||||
'dice_reward_config_record',
|
||||
];
|
||||
foreach ($bizTables as $table) {
|
||||
if (!$this->tableHasColumn($table, 'dept_id')) {
|
||||
continue;
|
||||
}
|
||||
$stats[$table . '_invalid_dept'] = Db::execute(
|
||||
"UPDATE `{$table}` SET dept_id = {$rootDeptId}
|
||||
WHERE dept_id IS NOT NULL AND dept_id > 0 AND dept_id NOT IN ({$inList})"
|
||||
);
|
||||
}
|
||||
|
||||
return $stats;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据管理员/玩家回填 dept_id
|
||||
*/
|
||||
public function backfillDataDeptId(): array
|
||||
{
|
||||
$stats = $this->repairOrphanDeptReferences();
|
||||
if ($this->tableHasColumn('dice_player', 'dept_id') && $this->tableHasColumn('dice_player', 'admin_id')) {
|
||||
$stats['dice_player'] = Db::execute(
|
||||
'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 OR p.dept_id = 0) AND u.dept_id IS NOT NULL AND u.dept_id > 0'
|
||||
);
|
||||
}
|
||||
$validDeptIds = SystemDept::column('id') ?: [];
|
||||
if (!empty($validDeptIds) && $this->tableHasColumn('dice_player', 'dept_id')) {
|
||||
$rootDeptId = (int) min($validDeptIds);
|
||||
$stats['dice_player_legacy'] = Db::table('dice_player')
|
||||
->where(function ($q) {
|
||||
$q->whereNull('dept_id')->whereOr('dept_id', 0);
|
||||
})
|
||||
->update(['dept_id' => $rootDeptId]);
|
||||
}
|
||||
$stats = array_merge($stats, $this->backfillRecordDeptIdByPlayer('dice_play_record'));
|
||||
$stats = array_merge($stats, $this->backfillRecordDeptIdByAdmin('dice_play_record'));
|
||||
$stats = array_merge($stats, $this->backfillRecordDeptIdByPlayer('dice_player_wallet_record'));
|
||||
$stats = array_merge($stats, $this->backfillRecordDeptIdByPlayer('dice_player_ticket_record'));
|
||||
$stats = array_merge($stats, $this->backfillRecordDeptIdByAdmin('dice_play_record_test'));
|
||||
if (!empty($validDeptIds) && $this->tableHasColumn('dice_play_record_test', 'dept_id')) {
|
||||
$rootDeptId = (int) min($validDeptIds);
|
||||
$stats['dice_play_record_test_legacy'] = Db::table('dice_play_record_test')
|
||||
->where(function ($q) {
|
||||
$q->whereNull('dept_id')->whereOr('dept_id', 0);
|
||||
})
|
||||
->update(['dept_id' => $rootDeptId]);
|
||||
}
|
||||
if ($this->tableHasColumn('dice_reward_config_record', 'dept_id')) {
|
||||
$stats['dice_reward_config_record'] = Db::execute(
|
||||
'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 OR r.dept_id = 0) AND u.dept_id IS NOT NULL AND u.dept_id > 0'
|
||||
);
|
||||
}
|
||||
return $stats;
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除渠道前关联数据统计
|
||||
*/
|
||||
public function getDestroyPreview(array $deptIds): array
|
||||
{
|
||||
$items = [];
|
||||
foreach ($deptIds as $deptId) {
|
||||
$deptId = (int) $deptId;
|
||||
if ($deptId <= 0) {
|
||||
continue;
|
||||
}
|
||||
$dept = SystemDept::find($deptId);
|
||||
$row = [
|
||||
'dept_id' => $deptId,
|
||||
'dept_name' => $dept ? $dept->name : '',
|
||||
'user_count' => SystemUser::where('dept_id', $deptId)->count(),
|
||||
'relations' => [],
|
||||
];
|
||||
foreach (self::RELATION_TABLES as $table => $meta) {
|
||||
if (!$this->tableHasColumn($table, 'dept_id')) {
|
||||
continue;
|
||||
}
|
||||
$count = $this->countByDept($table, $deptId);
|
||||
if ($count > 0) {
|
||||
$row['relations'][] = [
|
||||
'table' => $table,
|
||||
'label' => $meta['label'],
|
||||
'group' => $meta['group'],
|
||||
'count' => $count,
|
||||
];
|
||||
}
|
||||
}
|
||||
$items[] = $row;
|
||||
}
|
||||
return $items;
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除渠道及勾选的关联数据
|
||||
*
|
||||
* @param array $deleteTables 要删除的表名列表
|
||||
*/
|
||||
public function destroyDeptWithRelations(int $deptId, array $deleteTables): void
|
||||
{
|
||||
if ($deptId <= 0) {
|
||||
throw new ApiException('Invalid channel id');
|
||||
}
|
||||
$userCount = SystemUser::where('dept_id', $deptId)->count();
|
||||
if ($userCount > 0) {
|
||||
throw new ApiException('This channel has users, please delete or transfer them first');
|
||||
}
|
||||
$tablesToDelete = $this->sortTablesForDelete($deleteTables);
|
||||
Db::startTrans();
|
||||
try {
|
||||
foreach ($tablesToDelete as $table) {
|
||||
if (!$this->tableHasColumn($table, 'dept_id')) {
|
||||
continue;
|
||||
}
|
||||
Db::table($table)->where('dept_id', $deptId)->delete();
|
||||
}
|
||||
Db::name('sa_system_role_dept')->where('dept_id', $deptId)->delete();
|
||||
SystemDept::destroy($deptId, true);
|
||||
Db::commit();
|
||||
} catch (\Throwable $e) {
|
||||
Db::rollback();
|
||||
throw new ApiException('Channel delete failed: ' . $e->getMessage());
|
||||
}
|
||||
DiceRewardConfig::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>>
|
||||
*/
|
||||
private function defaultTemplateRows(string $table): array
|
||||
{
|
||||
$templateId = AdminScopeHelper::DEFAULT_TEMPLATE_DEPT;
|
||||
$rows = Db::table($table)->where('dept_id', $templateId)->select()->toArray();
|
||||
if (!empty($rows)) {
|
||||
return $rows;
|
||||
}
|
||||
return Db::table($table)->whereNull('dept_id')->select()->toArray();
|
||||
}
|
||||
|
||||
private function backfillRecordDeptIdByPlayer(string $table): array
|
||||
{
|
||||
if (!$this->tableHasColumn($table, 'dept_id') || !$this->tableHasColumn($table, 'player_id')) {
|
||||
return [];
|
||||
}
|
||||
return [
|
||||
$table => Db::execute(
|
||||
"UPDATE `{$table}` 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 OR r.dept_id = 0) AND p.dept_id IS NOT NULL AND p.dept_id > 0"
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
private function backfillRecordDeptIdByAdmin(string $table): array
|
||||
{
|
||||
if (!$this->tableHasColumn($table, 'dept_id') || !$this->tableHasColumn($table, 'admin_id')) {
|
||||
return [];
|
||||
}
|
||||
return [
|
||||
$table => Db::execute(
|
||||
"UPDATE `{$table}` 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 OR r.dept_id = 0) AND u.dept_id IS NOT NULL AND u.dept_id > 0"
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
private function countByDept(string $table, ?int $deptId): int
|
||||
{
|
||||
$query = Db::table($table);
|
||||
if ($deptId === null || $deptId === AdminScopeHelper::DEFAULT_TEMPLATE_DEPT) {
|
||||
$templateId = AdminScopeHelper::DEFAULT_TEMPLATE_DEPT;
|
||||
$query->where(function ($q) use ($templateId) {
|
||||
$q->where('dept_id', $templateId)->whereOr(function ($sub) {
|
||||
$sub->whereNull('dept_id');
|
||||
});
|
||||
});
|
||||
} else {
|
||||
$query->where('dept_id', $deptId);
|
||||
}
|
||||
return $query->count();
|
||||
}
|
||||
|
||||
private function tableHasColumn(string $table, string $column): bool
|
||||
{
|
||||
try {
|
||||
$fields = Db::getFields($table);
|
||||
return isset($fields[$column]);
|
||||
} catch (\Throwable $e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,8 @@ class DiceLotteryPoolConfigValidate extends BaseValidate
|
||||
*/
|
||||
protected $rule = [
|
||||
'name' => 'require',
|
||||
'remark' => 'max:200',
|
||||
'config_note' => 'max:500',
|
||||
't1_weight' => 'require',
|
||||
't2_weight' => 'require',
|
||||
't3_weight' => 'require',
|
||||
@@ -43,6 +45,8 @@ class DiceLotteryPoolConfigValidate extends BaseValidate
|
||||
protected $scene = [
|
||||
'save' => [
|
||||
'name',
|
||||
'remark',
|
||||
'config_note',
|
||||
't1_weight',
|
||||
't2_weight',
|
||||
't3_weight',
|
||||
@@ -51,6 +55,8 @@ class DiceLotteryPoolConfigValidate extends BaseValidate
|
||||
],
|
||||
'update' => [
|
||||
'name',
|
||||
'remark',
|
||||
'config_note',
|
||||
't1_weight',
|
||||
't2_weight',
|
||||
't3_weight',
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
// +----------------------------------------------------------------------
|
||||
namespace app\dice\validate\player;
|
||||
|
||||
use app\dice\model\player\DicePlayer;
|
||||
use plugin\saiadmin\basic\BaseValidate;
|
||||
|
||||
/**
|
||||
@@ -17,7 +18,7 @@ class DicePlayerValidate extends BaseValidate
|
||||
* 定义验证规则
|
||||
*/
|
||||
protected $rule = [
|
||||
'username' => 'require',
|
||||
'username' => 'require|unique:' . DicePlayer::class . ',username^dept_id',
|
||||
'name' => 'require',
|
||||
'phone' => 'require',
|
||||
'password' => 'require',
|
||||
@@ -30,6 +31,7 @@ class DicePlayerValidate extends BaseValidate
|
||||
*/
|
||||
protected $message = [
|
||||
'username' => '用户名必须填写',
|
||||
'username.unique' => 'PLAYER_USERNAME_DEPT_UNIQUE',
|
||||
'name' => '昵称必须填写',
|
||||
'phone' => '手机号必须填写',
|
||||
'password' => '密码必须填写',
|
||||
|
||||
@@ -6,11 +6,13 @@ return [
|
||||
// 登录成功返回的连接地址前缀,如 https://127.0.0.1:6777
|
||||
'login_url_base' => env('API_LOGIN_URL_BASE', 'https://127.0.0.1:6777'),
|
||||
// 游戏地址,用于 /api/v1/getGameUrl 返回拼接 token
|
||||
'game_url' => env('GAME_URL', 'dice-game.h55555game.top'),
|
||||
'game_url' => env('GAME_URL', 'dice-v3-game.h55555game.top'),
|
||||
// 按 username 存储的登录会话 Redis key 前缀,用于 token 中间件校验
|
||||
'session_username_prefix' => env('API_SESSION_USERNAME_PREFIX', 'api:user:session:'),
|
||||
// 登录会话过期时间(秒),默认 7 天
|
||||
'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_secret' => env('API_AUTH_TOKEN_SECRET', ''),
|
||||
// auth-token 时间戳允许误差(秒),防重放,默认 300 秒
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
|
||||
use Webman\Route;
|
||||
use app\api\middleware\ApiAccessLogMiddleware;
|
||||
use app\api\middleware\ApiKeyMiddleware;
|
||||
use app\api\middleware\AuthTokenMiddleware;
|
||||
use app\api\middleware\TokenMiddleware;
|
||||
|
||||
@@ -22,9 +23,10 @@ Route::group('/api/v1', function () {
|
||||
Route::any('/authToken', [app\api\controller\v1\AuthTokenController::class, 'index']);
|
||||
})->middleware([
|
||||
ApiAccessLogMiddleware::class,
|
||||
ApiKeyMiddleware::class,
|
||||
]);
|
||||
|
||||
// 平台 v1 接口:需在请求头携带 auth-token
|
||||
// 平台 v1 接口:需在请求头携带 api-key、auth-token
|
||||
Route::group('/api/v1', function () {
|
||||
Route::any('/getGameList', [app\api\controller\v1\GameController::class, 'getGameList']);
|
||||
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']);
|
||||
})->middleware([
|
||||
ApiAccessLogMiddleware::class,
|
||||
ApiKeyMiddleware::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`
|
||||
- `Accept: application/json`
|
||||
- `api-key: {api_key}`(**所有 `/api/v1/*` 必传**,与服务端 `.env` 中 `API_KEY` 一致)
|
||||
- `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 时间戳(秒)
|
||||
- 建议所有请求设置超时:连接超时 `3s`,读取超时 `10s`
|
||||
- 生产环境建议增加调用方 IP 白名单和重试退避机制(避免瞬时重试风暴)
|
||||
@@ -53,9 +58,9 @@
|
||||
常见错误码:
|
||||
|
||||
- `400` 参数错误
|
||||
- `401` 未携带 token
|
||||
- `401` 未携带 `api-key`、`auth-token` 或 `token`
|
||||
- `402` token 无效或过期
|
||||
- `403` 签名或鉴权失败
|
||||
- `403` `api-key` 无效、签名或鉴权失败
|
||||
- `404` 资源不存在
|
||||
- `422` 业务错误(如余额不足)
|
||||
- `500` 服务端异常
|
||||
@@ -64,11 +69,18 @@
|
||||
|
||||
## 4. 鉴权流程(平台级)
|
||||
|
||||
平台级凭证分两层:
|
||||
|
||||
- **`api-key`**:所有 `/api/v1/*` 接口必传,与服务端 `.env` 中 `API_KEY` 一致;可通过请求头、query、body 任一方式携带(详见 §2.1)。
|
||||
- **`auth-token`**:业务接口(除 `/api/v1/authToken` 外)必传,由 `/api/v1/authToken` 颁发。
|
||||
|
||||
`/api/v1/*` 接口调用前,先获取 `auth-token`。
|
||||
|
||||
### 4.1 获取 auth-token
|
||||
|
||||
- 路径: `GET /api/v1/authToken`
|
||||
- Header:
|
||||
- `api-key: {api_key}`(必传,与服务端 `.env` 中 `API_KEY` 一致)
|
||||
- 鉴权参数(Query):
|
||||
- `agent_id`:代理标识(商户标识)
|
||||
- `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`)
|
||||
- `secret` 不匹配即失败(`403`)
|
||||
- `time` 超出容差窗口即失败(`403`,默认容差 `300s`)
|
||||
- `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 携带:
|
||||
|
||||
```text
|
||||
api-key: {api_key}
|
||||
auth-token: {authtoken}
|
||||
```
|
||||
|
||||
### 4.2 完整调用链(推荐)
|
||||
|
||||
1. 计算 `signature = md5(agent_id + secret + time)`
|
||||
2. 调用 `GET /api/v1/authToken` 获取 `authtoken`
|
||||
3. 在 Header 添加 `auth-token: {authtoken}`
|
||||
2. 调用 `GET /api/v1/authToken`(Header 携带 `api-key`)获取 `authtoken`
|
||||
3. 在 Header 添加 `api-key: {api_key}` 与 `auth-token: {authtoken}`
|
||||
4. 调用业务接口(如 `getPlayerInfo`、`setPlayerWallet`、`getGameUrl`、`getPlayerGameRecord`、`getPlayerWalletRecord`、`getPlayerTicketRecord`)
|
||||
5. 若返回 `402`,重新获取 `authtoken` 后重试一次
|
||||
|
||||
@@ -155,12 +169,13 @@ auth-token: {authtoken}
|
||||
|
||||
## 5. 游戏相关接口
|
||||
|
||||
以下接口均需 Header: `auth-token`。
|
||||
以下接口均需 Header:`api-key` + `auth-token`(`api-key` 也可放 query/body,参见 §2.1)。
|
||||
|
||||
## 5.1 获取游戏列表(已支持)
|
||||
|
||||
- 路径: `POST /api/v1/getGameList`
|
||||
- Header:
|
||||
- `api-key: {api_key}`
|
||||
- `auth-token: {authtoken}`
|
||||
- Body 参数:
|
||||
- `lang`(可选):`zh`/`en`,默认 `zh`
|
||||
@@ -240,6 +255,7 @@ auth-token: {authtoken}
|
||||
|
||||
- 路径: `POST /api/v1/getGameHall`
|
||||
- Header:
|
||||
- `api-key: {api_key}`
|
||||
- `auth-token: {authtoken}`
|
||||
- Body 参数:
|
||||
- `lang`(可选):`zh`/`en`,默认 `zh`
|
||||
@@ -316,9 +332,11 @@ auth-token: {authtoken}
|
||||
## 5.3 获取某个游戏地址(已支持)
|
||||
|
||||
- 路径: `POST /api/v1/getGameUrl`
|
||||
- Header:
|
||||
- `api-key: {api_key}`
|
||||
- `auth-token: {authtoken}`
|
||||
- Body 参数:
|
||||
- `username`(必填):玩家账号(不存在会自动创建)
|
||||
- `password`(可选):默认 `123456`
|
||||
- `time`(可选):不传则服务端取当前时间戳
|
||||
- `lang`(可选):`zh`/`en`,默认 `zh`
|
||||
|
||||
@@ -349,6 +367,7 @@ auth-token: {authtoken}
|
||||
|
||||
- 路径: `POST /api/v1/getPlayerGameRecord`
|
||||
- Header:
|
||||
- `api-key: {api_key}`
|
||||
- `auth-token: {authtoken}`
|
||||
- Body 参数:
|
||||
- `username`(可选):玩家账号;不传则**不按玩家筛选**(返回库内符合条件的记录,请谨慎使用)
|
||||
@@ -396,7 +415,7 @@ auth-token: {authtoken}
|
||||
|
||||
## 7. 钱包相关接口
|
||||
|
||||
以下接口均需 Header: `auth-token`。
|
||||
以下接口均需 Header:`api-key` + `auth-token`(`api-key` 也可放 query/body,参见 §2.1)。
|
||||
|
||||
### 7.1 查询余额(已支持)
|
||||
|
||||
@@ -430,6 +449,7 @@ auth-token: {authtoken}
|
||||
|
||||
- 路径: `POST /api/v1/getPlayerWalletRecord`
|
||||
- Header:
|
||||
- `api-key: {api_key}`
|
||||
- `auth-token: {authtoken}`
|
||||
- Body 参数:
|
||||
- `username`(可选):玩家账号;不传则**不按玩家筛选**
|
||||
@@ -445,6 +465,7 @@ auth-token: {authtoken}
|
||||
|
||||
- 路径: `POST /api/v1/getPlayerTicketRecord`
|
||||
- Header:
|
||||
- `api-key: {api_key}`
|
||||
- `auth-token: {authtoken}`
|
||||
- Body 参数:与 **7.4** 相同(`username`、`start_create_time`、`end_create_time`、`limit`)
|
||||
- 返回说明:
|
||||
@@ -462,7 +483,8 @@ auth-token: {authtoken}
|
||||
- `provider`:`Dicey Fun`
|
||||
- `provider_code`:`DF`
|
||||
- `agent_id`:`5ef059938ba799aaa845e1c2e8a762bd`
|
||||
- `secret`:签名密钥(双方约定)
|
||||
- `secret`:签名密钥(双方约定,对应服务端 `.env` 中 `API_AUTH_TOKEN_SECRET`)
|
||||
- `api_key`:所有 `/api/v1/*` 请求必传的 `api-key`(对应服务端 `.env` 中 `API_KEY`)
|
||||
- `agent_token`:`[我来填]`(如需额外业务层 token)
|
||||
- `game_url`:游戏前端域名/地址
|
||||
- `lobby_url`:大厅地址(可选)
|
||||
@@ -482,8 +504,8 @@ auth-token: {authtoken}
|
||||
|
||||
## 10. 对接时序(建议)
|
||||
|
||||
1. 平台分配 `agent_id`、`secret`
|
||||
2. 第三方调用 `/api/v1/authToken` 获取 `authtoken`
|
||||
1. 平台分配 `agent_id`、`secret`、`api_key`
|
||||
2. 第三方调用 `/api/v1/authToken`(Header 携带 `api-key`)获取 `authtoken`
|
||||
3. 第三方调用 `/api/v1/getGameHall` 或 `/api/v1/getGameList` 获取大厅/游戏信息
|
||||
4. 第三方调用 `/api/v1/getPlayerInfo`(可选,检查用户与余额)
|
||||
5. 第三方调用 `/api/v1/setPlayerWallet` 进行额度转入(如有)
|
||||
@@ -498,7 +520,8 @@ auth-token: {authtoken}
|
||||
### 11.1 获取 auth-token
|
||||
|
||||
```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
|
||||
curl --location --request POST 'https://{your-domain}/api/v1/getGameUrl' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--header 'api-key: {api_key}' \
|
||||
--header 'auth-token: {authtoken}' \
|
||||
--data-raw '{
|
||||
"username":"test_player_001",
|
||||
@@ -526,6 +550,7 @@ curl --location --request POST 'https://{your-domain}/api/v1/getGameUrl' \
|
||||
```bash
|
||||
curl --location --request POST 'https://{your-domain}/api/v1/getGameList' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--header 'api-key: {api_key}' \
|
||||
--header 'auth-token: {authtoken}' \
|
||||
--data-raw '{
|
||||
"lang":"zh"
|
||||
@@ -537,6 +562,7 @@ curl --location --request POST 'https://{your-domain}/api/v1/getGameList' \
|
||||
```bash
|
||||
curl --location --request POST 'https://{your-domain}/api/v1/getGameList' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--header 'api-key: {api_key}' \
|
||||
--header 'auth-token: {authtoken}' \
|
||||
--data-raw '{
|
||||
"lang":"en"
|
||||
@@ -548,6 +574,7 @@ curl --location --request POST 'https://{your-domain}/api/v1/getGameList' \
|
||||
```bash
|
||||
curl --location --request POST 'https://{your-domain}/api/v1/getGameHall' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--header 'api-key: {api_key}' \
|
||||
--header 'auth-token: {authtoken}' \
|
||||
--data-raw '{
|
||||
"lang":"zh"
|
||||
@@ -559,6 +586,7 @@ curl --location --request POST 'https://{your-domain}/api/v1/getGameHall' \
|
||||
```bash
|
||||
curl --location --request POST 'https://{your-domain}/api/v1/setPlayerWallet' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--header 'api-key: {api_key}' \
|
||||
--header 'auth-token: {authtoken}' \
|
||||
--data-raw '{
|
||||
"username":"test_player_001",
|
||||
@@ -571,6 +599,7 @@ curl --location --request POST 'https://{your-domain}/api/v1/setPlayerWallet' \
|
||||
```bash
|
||||
curl --location --request POST 'https://{your-domain}/api/v1/getPlayerInfo' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--header 'api-key: {api_key}' \
|
||||
--header 'auth-token: {authtoken}' \
|
||||
--data-raw '{
|
||||
"username":"test_player_001"
|
||||
@@ -582,6 +611,7 @@ curl --location --request POST 'https://{your-domain}/api/v1/getPlayerInfo' \
|
||||
```bash
|
||||
curl --location --request POST 'https://{your-domain}/api/v1/getPlayerGameRecord' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--header 'api-key: {api_key}' \
|
||||
--header 'auth-token: {authtoken}' \
|
||||
--data-raw '{
|
||||
"username":"test_player_001",
|
||||
@@ -594,6 +624,7 @@ curl --location --request POST 'https://{your-domain}/api/v1/getPlayerGameRecord
|
||||
```bash
|
||||
curl --location --request POST 'https://{your-domain}/api/v1/getPlayerWalletRecord' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--header 'api-key: {api_key}' \
|
||||
--header 'auth-token: {authtoken}' \
|
||||
--data-raw '{
|
||||
"username":"test_player_001",
|
||||
@@ -606,6 +637,7 @@ curl --location --request POST 'https://{your-domain}/api/v1/getPlayerWalletReco
|
||||
```bash
|
||||
curl --location --request POST 'https://{your-domain}/api/v1/getPlayerTicketRecord' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--header 'api-key: {api_key}' \
|
||||
--header 'auth-token: {authtoken}' \
|
||||
--data-raw '{
|
||||
"username":"test_player_001",
|
||||
|
||||
@@ -26,7 +26,12 @@
|
||||
- Unified headers:
|
||||
- `Content-Type: 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`)
|
||||
- `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)
|
||||
- Recommended timeouts: connect timeout `3s`, read timeout `10s`
|
||||
- Production recommendation: add caller IP whitelist and retry backoff (to avoid burst retry storms)
|
||||
@@ -53,9 +58,9 @@ Notes:
|
||||
Common error codes:
|
||||
|
||||
- `400` Invalid parameters
|
||||
- `401` Missing token
|
||||
- `401` Missing `api-key`, `auth-token` or `token`
|
||||
- `402` Token invalid or expired
|
||||
- `403` Signature or authentication failed
|
||||
- `403` Invalid `api-key`, signature or authentication failed
|
||||
- `404` Resource not found
|
||||
- `422` Business error (e.g., insufficient balance)
|
||||
- `500` Server exception
|
||||
@@ -64,11 +69,18 @@ Common error codes:
|
||||
|
||||
## 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.
|
||||
|
||||
### 4.1 Get auth-token
|
||||
|
||||
- Path: `GET /api/v1/authToken`
|
||||
- Header:
|
||||
- `api-key: {api_key}` (Required, must match `API_KEY` in server `.env`)
|
||||
- Auth parameters (Query):
|
||||
- `agent_id`: Agent identifier (merchant identifier)
|
||||
- `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):
|
||||
|
||||
- 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`)
|
||||
- `secret` mismatch => fail (`403`)
|
||||
- `time` outside tolerance window => fail (`403`, default tolerance `300s`)
|
||||
- `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:
|
||||
|
||||
@@ -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
|
||||
api-key: {api_key}
|
||||
auth-token: {authtoken}
|
||||
```
|
||||
|
||||
### 4.2 Full Call Chain (Recommended)
|
||||
|
||||
1. Compute `signature = md5(agent_id + secret + time)`
|
||||
2. Call `GET /api/v1/authToken` to obtain `authtoken`
|
||||
3. Add header `auth-token: {authtoken}`
|
||||
2. Call `GET /api/v1/authToken` (Header `api-key`) to obtain `authtoken`
|
||||
3. Add headers `api-key: {api_key}` and `auth-token: {authtoken}`
|
||||
4. Call business endpoints (e.g., `getPlayerInfo`, `setPlayerWallet`, `getGameUrl`, `getPlayerGameRecord`, `getPlayerWalletRecord`, `getPlayerTicketRecord`)
|
||||
5. If `402` is returned, re-fetch `authtoken` and retry once
|
||||
|
||||
@@ -155,12 +169,13 @@ auth-token: {authtoken}
|
||||
|
||||
## 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)
|
||||
|
||||
- Path: `POST /api/v1/getGameList`
|
||||
- Header:
|
||||
- `api-key: {api_key}`
|
||||
- `auth-token: {authtoken}`
|
||||
- Body parameters:
|
||||
- `lang` (optional): `zh`/`en`, default `zh`
|
||||
@@ -240,6 +255,7 @@ Success example (`lang=en`):
|
||||
|
||||
- Path: `POST /api/v1/getGameHall`
|
||||
- Header:
|
||||
- `api-key: {api_key}`
|
||||
- `auth-token: {authtoken}`
|
||||
- Body parameters:
|
||||
- `lang` (optional): `zh`/`en`, default `zh`
|
||||
@@ -316,9 +332,11 @@ Success example (`lang=en`):
|
||||
## 5.3 Get Game URL (Supported)
|
||||
|
||||
- Path: `POST /api/v1/getGameUrl`
|
||||
- Header:
|
||||
- `api-key: {api_key}`
|
||||
- `auth-token: {authtoken}`
|
||||
- Body parameters:
|
||||
- `username` (required): Player username (auto-created if not exists)
|
||||
- `password` (optional): default `123456`
|
||||
- `time` (optional): if omitted, server uses current timestamp
|
||||
- `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`
|
||||
- Header:
|
||||
- `api-key: {api_key}`
|
||||
- `auth-token: {authtoken}`
|
||||
- Body parameters:
|
||||
- `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
|
||||
|
||||
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)
|
||||
|
||||
@@ -430,6 +449,7 @@ If the integrator’s wallet flow requires “return lobby URL after transfer”
|
||||
|
||||
- Path: `POST /api/v1/getPlayerWalletRecord`
|
||||
- Header:
|
||||
- `api-key: {api_key}`
|
||||
- `auth-token: {authtoken}`
|
||||
- Body parameters:
|
||||
- `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`
|
||||
- Header:
|
||||
- `api-key: {api_key}`
|
||||
- `auth-token: {authtoken}`
|
||||
- Body parameters: Same as **7.4** (`username`, `start_create_time`, `end_create_time`, `limit`)
|
||||
- Response notes:
|
||||
@@ -462,7 +483,8 @@ It is recommended to configure the following fields in the integration parameter
|
||||
- `provider`: `Dicey Fun`
|
||||
- `provider_code`: `DF`
|
||||
- `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)
|
||||
- `game_url`: Game frontend domain/URL
|
||||
- `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)
|
||||
|
||||
1. Platform assigns `agent_id` and `secret`
|
||||
2. Third party calls `/api/v1/authToken` to obtain `authtoken`
|
||||
1. Platform assigns `agent_id`, `secret` and `api_key`
|
||||
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
|
||||
4. Third party calls `/api/v1/getPlayerInfo` (optional, check user and balance)
|
||||
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
|
||||
|
||||
```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:
|
||||
@@ -514,6 +537,7 @@ During integration testing, it is recommended to print the following values loca
|
||||
```bash
|
||||
curl --location --request POST 'https://{your-domain}/api/v1/getGameUrl' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--header 'api-key: {api_key}' \
|
||||
--header 'auth-token: {authtoken}' \
|
||||
--data-raw '{
|
||||
"username":"test_player_001",
|
||||
@@ -526,6 +550,7 @@ curl --location --request POST 'https://{your-domain}/api/v1/getGameUrl' \
|
||||
```bash
|
||||
curl --location --request POST 'https://{your-domain}/api/v1/getGameList' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--header 'api-key: {api_key}' \
|
||||
--header 'auth-token: {authtoken}' \
|
||||
--data-raw '{
|
||||
"lang":"zh"
|
||||
@@ -537,6 +562,7 @@ curl --location --request POST 'https://{your-domain}/api/v1/getGameList' \
|
||||
```bash
|
||||
curl --location --request POST 'https://{your-domain}/api/v1/getGameList' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--header 'api-key: {api_key}' \
|
||||
--header 'auth-token: {authtoken}' \
|
||||
--data-raw '{
|
||||
"lang":"en"
|
||||
@@ -548,6 +574,7 @@ curl --location --request POST 'https://{your-domain}/api/v1/getGameList' \
|
||||
```bash
|
||||
curl --location --request POST 'https://{your-domain}/api/v1/getGameHall' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--header 'api-key: {api_key}' \
|
||||
--header 'auth-token: {authtoken}' \
|
||||
--data-raw '{
|
||||
"lang":"zh"
|
||||
@@ -559,6 +586,7 @@ curl --location --request POST 'https://{your-domain}/api/v1/getGameHall' \
|
||||
```bash
|
||||
curl --location --request POST 'https://{your-domain}/api/v1/setPlayerWallet' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--header 'api-key: {api_key}' \
|
||||
--header 'auth-token: {authtoken}' \
|
||||
--data-raw '{
|
||||
"username":"test_player_001",
|
||||
@@ -571,6 +599,7 @@ curl --location --request POST 'https://{your-domain}/api/v1/setPlayerWallet' \
|
||||
```bash
|
||||
curl --location --request POST 'https://{your-domain}/api/v1/getPlayerInfo' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--header 'api-key: {api_key}' \
|
||||
--header 'auth-token: {authtoken}' \
|
||||
--data-raw '{
|
||||
"username":"test_player_001"
|
||||
@@ -582,6 +611,7 @@ curl --location --request POST 'https://{your-domain}/api/v1/getPlayerInfo' \
|
||||
```bash
|
||||
curl --location --request POST 'https://{your-domain}/api/v1/getPlayerGameRecord' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--header 'api-key: {api_key}' \
|
||||
--header 'auth-token: {authtoken}' \
|
||||
--data-raw '{
|
||||
"username":"test_player_001",
|
||||
@@ -594,6 +624,7 @@ curl --location --request POST 'https://{your-domain}/api/v1/getPlayerGameRecord
|
||||
```bash
|
||||
curl --location --request POST 'https://{your-domain}/api/v1/getPlayerWalletRecord' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--header 'api-key: {api_key}' \
|
||||
--header 'auth-token: {authtoken}' \
|
||||
--data-raw '{
|
||||
"username":"test_player_001",
|
||||
@@ -606,6 +637,7 @@ curl --location --request POST 'https://{your-domain}/api/v1/getPlayerWalletReco
|
||||
```bash
|
||||
curl --location --request POST 'https://{your-domain}/api/v1/getPlayerTicketRecord' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--header 'api-key: {api_key}' \
|
||||
--header 'auth-token: {authtoken}' \
|
||||
--data-raw '{
|
||||
"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
|
||||
@@ -27,7 +27,6 @@ class UserInfoCache
|
||||
'expire' => 60 * 60 * 4,
|
||||
'dept' => 'saiadmin:user_cache:dept_',
|
||||
'role' => 'saiadmin:user_cache:role_',
|
||||
'post' => 'saiadmin:user_cache:post_',
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -73,11 +72,6 @@ class UserInfoCache
|
||||
$tags[] = $cache['role'] . $role['id'];
|
||||
}
|
||||
}
|
||||
if (!empty($data['postList'])) {
|
||||
foreach ($data['postList'] as $post) {
|
||||
$tags[] = $cache['post'] . $post['id'];
|
||||
}
|
||||
}
|
||||
Cache::tag($tags)->set($cache['prefix'] . $uid, $data, $cache['expire']);
|
||||
return $data;
|
||||
}
|
||||
@@ -125,21 +119,4 @@ class UserInfoCache
|
||||
return Cache::tag($tags)->clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理岗位下所有用户缓存
|
||||
*/
|
||||
public static function clearUserInfoByPostId($post_id): bool
|
||||
{
|
||||
$cache = static::cacheConfig();
|
||||
if (is_array($post_id)) {
|
||||
$tags = [];
|
||||
foreach ($post_id as $id) {
|
||||
$tags[] = $cache['post'] . $id;
|
||||
}
|
||||
} else {
|
||||
$tags = $cache['post'] . $post_id;
|
||||
}
|
||||
return Cache::tag($tags)->clear();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,7 @@ use support\Request;
|
||||
use support\Response;
|
||||
|
||||
/**
|
||||
* 部门控制器
|
||||
* 渠道控制器
|
||||
*/
|
||||
class SystemDeptController extends BaseController
|
||||
{
|
||||
@@ -33,7 +33,7 @@ class SystemDeptController extends BaseController
|
||||
* @param Request $request
|
||||
* @return Response
|
||||
*/
|
||||
#[Permission('部门数据列表', 'core:dept:index')]
|
||||
#[Permission('渠道数据列表', 'core:dept:index')]
|
||||
public function index(Request $request) : Response
|
||||
{
|
||||
$where = $request->more([
|
||||
@@ -50,7 +50,7 @@ class SystemDeptController extends BaseController
|
||||
* @param Request $request
|
||||
* @return Response
|
||||
*/
|
||||
#[Permission('部门数据读取', 'core:dept:read')]
|
||||
#[Permission('渠道数据读取', 'core:dept:read')]
|
||||
public function read(Request $request) : Response
|
||||
{
|
||||
$id = $request->input('id', '');
|
||||
@@ -68,7 +68,7 @@ class SystemDeptController extends BaseController
|
||||
* @param Request $request
|
||||
* @return Response
|
||||
*/
|
||||
#[Permission('部门数据添加', 'core:dept:save')]
|
||||
#[Permission('渠道数据添加', 'core:dept:save')]
|
||||
public function save(Request $request): Response
|
||||
{
|
||||
$data = $request->post();
|
||||
@@ -86,7 +86,7 @@ class SystemDeptController extends BaseController
|
||||
* @param Request $request
|
||||
* @return Response
|
||||
*/
|
||||
#[Permission('部门数据修改','core:dept:update')]
|
||||
#[Permission('渠道数据修改','core:dept:update')]
|
||||
public function update(Request $request): Response
|
||||
{
|
||||
$data = $request->post();
|
||||
@@ -104,23 +104,59 @@ class SystemDeptController extends BaseController
|
||||
* @param Request $request
|
||||
* @return Response
|
||||
*/
|
||||
#[Permission('部门数据删除','core:dept:destroy')]
|
||||
#[Permission('渠道数据删除','core:dept:destroy')]
|
||||
public function destroy(Request $request) : Response
|
||||
{
|
||||
$ids = $request->post('ids', '');
|
||||
// DELETE + JSON body 须用 input();post() 仅表单 POST 有效(与 SystemUserController 一致)
|
||||
$ids = $request->input('ids', '');
|
||||
if (empty($ids)) {
|
||||
return $this->fail('please select data to delete');
|
||||
}
|
||||
$deleteTables = $request->input('delete_tables', []);
|
||||
if (!is_array($deleteTables)) {
|
||||
$deleteTables = [];
|
||||
}
|
||||
$idList = is_array($ids) ? $ids : explode(',', (string) $ids);
|
||||
if (!empty($deleteTables)) {
|
||||
foreach ($idList as $deptId) {
|
||||
$this->logic->destroyWithRelations((int) $deptId, $deleteTables);
|
||||
}
|
||||
return $this->success('delete success');
|
||||
}
|
||||
$result = $this->logic->destroy($ids);
|
||||
if ($result) {
|
||||
return $this->success('delete success');
|
||||
} else {
|
||||
return $this->fail('delete failed');
|
||||
}
|
||||
return $this->fail('delete failed');
|
||||
}
|
||||
|
||||
/**
|
||||
* 可操作部门
|
||||
* 删除渠道前关联数据预览
|
||||
*/
|
||||
#[Permission('渠道数据删除', 'core:dept:destroy')]
|
||||
public function destroyPreview(Request $request): Response
|
||||
{
|
||||
$ids = $request->input('ids', '');
|
||||
if ($ids === '' || $ids === null) {
|
||||
return $this->fail('please select data');
|
||||
}
|
||||
$idList = is_array($ids) ? $ids : explode(',', (string) $ids);
|
||||
$data = $this->logic->getDestroyPreview($idList);
|
||||
return $this->success($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 为所有渠道补齐默认配置
|
||||
*/
|
||||
#[Permission('渠道数据修改', 'core:dept:update')]
|
||||
public function syncChannelConfigs(Request $request): Response
|
||||
{
|
||||
$data = $this->logic->syncAllChannelConfigs();
|
||||
return $this->success($data, 'sync success');
|
||||
}
|
||||
|
||||
/**
|
||||
* 可操作渠道
|
||||
* @param Request $request
|
||||
* @return Response
|
||||
*/
|
||||
|
||||
@@ -32,6 +32,7 @@ class SystemLogController extends BaseController
|
||||
['username', ''],
|
||||
['status', ''],
|
||||
['ip', ''],
|
||||
['dept_id', ''],
|
||||
]);
|
||||
$logic = new SystemLoginLogLogic();
|
||||
$query = $logic->search($where);
|
||||
@@ -71,6 +72,7 @@ class SystemLogController extends BaseController
|
||||
['service_name', ''],
|
||||
['router', ''],
|
||||
['ip', ''],
|
||||
['dept_id', ''],
|
||||
]);
|
||||
$logic = new SystemOperLogLogic();
|
||||
$logic->init($this->adminInfo);
|
||||
|
||||
@@ -1,177 +0,0 @@
|
||||
<?php
|
||||
// +----------------------------------------------------------------------
|
||||
// | saiadmin [ saiadmin快速开发框架 ]
|
||||
// +----------------------------------------------------------------------
|
||||
// | Author: sai <1430792918@qq.com>
|
||||
// +----------------------------------------------------------------------
|
||||
namespace plugin\saiadmin\app\controller\system;
|
||||
|
||||
use plugin\saiadmin\basic\BaseController;
|
||||
use plugin\saiadmin\app\logic\system\SystemPostLogic;
|
||||
use plugin\saiadmin\app\validate\system\SystemPostValidate;
|
||||
use plugin\saiadmin\service\Permission;
|
||||
use support\Request;
|
||||
use support\Response;
|
||||
|
||||
/**
|
||||
* 岗位信息控制器
|
||||
*/
|
||||
class SystemPostController extends BaseController
|
||||
{
|
||||
/**
|
||||
* 构造
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->logic = new SystemPostLogic();
|
||||
$this->validate = new SystemPostValidate;
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* 数据列表
|
||||
* @param Request $request
|
||||
* @return Response
|
||||
*/
|
||||
#[Permission('岗位数据列表', 'core:post:index')]
|
||||
public function index(Request $request): Response
|
||||
{
|
||||
$where = $request->more([
|
||||
['name', ''],
|
||||
['code', ''],
|
||||
['status', ''],
|
||||
]);
|
||||
$query = $this->logic->search($where);
|
||||
$data = $this->logic->getList($query);
|
||||
return $this->success($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取数据
|
||||
* @param Request $request
|
||||
* @return Response
|
||||
*/
|
||||
#[Permission('岗位数据读取', 'core:post:read')]
|
||||
public function read(Request $request): Response
|
||||
{
|
||||
$id = $request->input('id', '');
|
||||
$model = $this->logic->read($id);
|
||||
if ($model) {
|
||||
$data = is_array($model) ? $model : $model->toArray();
|
||||
return $this->success($data);
|
||||
} else {
|
||||
return $this->fail('not found');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存数据
|
||||
* @param Request $request
|
||||
* @return Response
|
||||
*/
|
||||
#[Permission('岗位数据添加', 'core:post:save')]
|
||||
public function save(Request $request): Response
|
||||
{
|
||||
$data = $request->post();
|
||||
$this->validate('save', $data);
|
||||
$result = $this->logic->add($data);
|
||||
if ($result) {
|
||||
return $this->success('add success');
|
||||
} else {
|
||||
return $this->fail('add failed');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新数据
|
||||
* @param Request $request
|
||||
* @return Response
|
||||
*/
|
||||
#[Permission('岗位数据修改', 'core:post:update')]
|
||||
public function update(Request $request): Response
|
||||
{
|
||||
$data = $request->post();
|
||||
$this->validate('update', $data);
|
||||
$result = $this->logic->edit($data['id'], $data);
|
||||
if ($result) {
|
||||
return $this->success('update success');
|
||||
} else {
|
||||
return $this->fail('update failed');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除数据
|
||||
* @param Request $request
|
||||
* @return Response
|
||||
*/
|
||||
#[Permission('岗位数据删除', 'core:post:destroy')]
|
||||
public function destroy(Request $request): Response
|
||||
{
|
||||
$ids = $request->post('ids', '');
|
||||
if (empty($ids)) {
|
||||
return $this->fail('please select data to delete');
|
||||
}
|
||||
$result = $this->logic->destroy($ids);
|
||||
if ($result) {
|
||||
return $this->success('delete success');
|
||||
} else {
|
||||
return $this->fail('delete failed');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 导入数据
|
||||
* @param Request $request
|
||||
* @return Response
|
||||
*/
|
||||
#[Permission('岗位数据导入', 'core:post:import')]
|
||||
public function import(Request $request): Response
|
||||
{
|
||||
$file = current($request->file());
|
||||
if (!$file || !$file->isValid()) {
|
||||
return $this->fail('uploaded file not found');
|
||||
}
|
||||
$this->logic->import($file);
|
||||
return $this->success('import success');
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出数据
|
||||
* @param Request $request
|
||||
* @return Response
|
||||
*/
|
||||
#[Permission('岗位数据导出', 'core:post:export')]
|
||||
public function export(Request $request): Response
|
||||
{
|
||||
$where = $request->more([
|
||||
['name', ''],
|
||||
['code', ''],
|
||||
['status', ''],
|
||||
]);
|
||||
return $this->logic->export($where);
|
||||
}
|
||||
|
||||
/**
|
||||
* 下载导入模板
|
||||
* @return Response
|
||||
*/
|
||||
public function downloadTemplate(): Response
|
||||
{
|
||||
$file_name = "template.xlsx";
|
||||
return downloadFile($file_name);
|
||||
}
|
||||
|
||||
/**
|
||||
* 可操作岗位
|
||||
* @param Request $request
|
||||
* @return Response
|
||||
*/
|
||||
public function accessPost(Request $request): Response
|
||||
{
|
||||
$where = ['status' => 1];
|
||||
$data = $this->logic->accessPost($where);
|
||||
return $this->success($data);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -6,10 +6,8 @@
|
||||
// +----------------------------------------------------------------------
|
||||
namespace plugin\saiadmin\app\controller\system;
|
||||
|
||||
use plugin\saiadmin\app\model\system\SystemUserRole;
|
||||
use app\dice\helper\AdminScopeHelper;
|
||||
use plugin\saiadmin\basic\BaseController;
|
||||
use plugin\saiadmin\app\cache\UserInfoCache;
|
||||
use plugin\saiadmin\app\model\system\SystemUser;
|
||||
use plugin\saiadmin\app\validate\system\SystemRoleValidate;
|
||||
use plugin\saiadmin\app\logic\system\SystemRoleLogic;
|
||||
use plugin\saiadmin\service\Permission;
|
||||
@@ -17,13 +15,10 @@ use support\Request;
|
||||
use support\Response;
|
||||
|
||||
/**
|
||||
* 角色控制器
|
||||
* 角色控制器(按渠道隔离)
|
||||
*/
|
||||
class SystemRoleController extends BaseController
|
||||
{
|
||||
/**
|
||||
* 构造
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->logic = new SystemRoleLogic();
|
||||
@@ -31,11 +26,6 @@ class SystemRoleController extends BaseController
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* 数据列表
|
||||
* @param Request $request
|
||||
* @return Response
|
||||
*/
|
||||
#[Permission('角色数据列表', 'core:role:index')]
|
||||
public function index(Request $request): Response
|
||||
{
|
||||
@@ -44,19 +34,14 @@ class SystemRoleController extends BaseController
|
||||
['code', ''],
|
||||
['status', ''],
|
||||
]);
|
||||
$query = $this->logic->search($where);
|
||||
$levelArr = array_column($this->adminInfo['roleList'], 'level');
|
||||
$maxLevel = max($levelArr);
|
||||
$query->where('level', '<', $maxLevel);
|
||||
$data = $this->logic->getList($query);
|
||||
$requestDeptId = AdminScopeHelper::pickRequestDeptId(
|
||||
$request->input('dept_id'),
|
||||
$request->all()
|
||||
);
|
||||
$data = $this->logic->indexList($where, $requestDeptId);
|
||||
return $this->success($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取数据
|
||||
* @param Request $request
|
||||
* @return Response
|
||||
*/
|
||||
#[Permission('角色数据读取', 'core:role:read')]
|
||||
public function read(Request $request): Response
|
||||
{
|
||||
@@ -64,53 +49,49 @@ class SystemRoleController extends BaseController
|
||||
$model = $this->logic->read($id);
|
||||
if ($model) {
|
||||
$data = is_array($model) ? $model : $model->toArray();
|
||||
$role = $this->logic->model->find($id);
|
||||
if ($role) {
|
||||
$this->logic->assertRoleWritable($role);
|
||||
}
|
||||
return $this->success($data);
|
||||
} else {
|
||||
}
|
||||
return $this->fail('not found');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存数据
|
||||
* @param Request $request
|
||||
* @return Response
|
||||
*/
|
||||
#[Permission('角色数据添加', 'core:role:save')]
|
||||
public function save(Request $request): Response
|
||||
{
|
||||
$data = $request->post();
|
||||
$data['dept_id'] = $this->logic->resolveRequestDeptId(
|
||||
AdminScopeHelper::pickRequestDeptId($data['dept_id'] ?? null, $data)
|
||||
);
|
||||
$this->validate('save', $data);
|
||||
$result = $this->logic->add($data);
|
||||
if ($result) {
|
||||
return $this->success('add success');
|
||||
} else {
|
||||
}
|
||||
return $this->fail('add failed');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新数据
|
||||
* @param Request $request
|
||||
* @return Response
|
||||
*/
|
||||
#[Permission('角色数据修改', 'core:role:update')]
|
||||
public function update(Request $request): Response
|
||||
{
|
||||
$data = $request->post();
|
||||
$role = $this->logic->model->find($data['id'] ?? 0);
|
||||
if ($role) {
|
||||
$this->logic->assertRoleWritable($role);
|
||||
if (!isset($data['dept_id']) || $data['dept_id'] === '' || $data['dept_id'] === null) {
|
||||
$data['dept_id'] = $role->dept_id;
|
||||
}
|
||||
}
|
||||
$this->validate('update', $data);
|
||||
$result = $this->logic->edit($data['id'], $data);
|
||||
if ($result) {
|
||||
return $this->success('update success');
|
||||
} else {
|
||||
}
|
||||
return $this->fail('update failed');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除数据
|
||||
* @param Request $request
|
||||
* @return Response
|
||||
*/
|
||||
#[Permission('角色数据删除', 'core:role:destroy')]
|
||||
public function destroy(Request $request): Response
|
||||
{
|
||||
@@ -121,16 +102,10 @@ class SystemRoleController extends BaseController
|
||||
$result = $this->logic->destroy($ids);
|
||||
if ($result) {
|
||||
return $this->success('delete success');
|
||||
} else {
|
||||
}
|
||||
return $this->fail('delete failed');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据角色获取菜单
|
||||
* @param Request $request
|
||||
* @return Response
|
||||
*/
|
||||
#[Permission('角色数据列表', 'core:role:index')]
|
||||
public function getMenuByRole(Request $request): Response
|
||||
{
|
||||
@@ -139,11 +114,6 @@ class SystemRoleController extends BaseController
|
||||
return $this->success($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 菜单权限
|
||||
* @param Request $request
|
||||
* @return Response
|
||||
*/
|
||||
#[Permission('角色菜单权限', 'core:role:menu')]
|
||||
public function menuPermission(Request $request): Response
|
||||
{
|
||||
@@ -153,16 +123,14 @@ class SystemRoleController extends BaseController
|
||||
return $this->success('operation success');
|
||||
}
|
||||
|
||||
/**
|
||||
* 可操作角色
|
||||
* @param Request $request
|
||||
* @return Response
|
||||
*/
|
||||
public function accessRole(Request $request): Response
|
||||
{
|
||||
$where = ['status' => 1];
|
||||
$data = $this->logic->accessRole($where);
|
||||
$requestDeptId = AdminScopeHelper::pickRequestDeptId(
|
||||
$request->input('dept_id'),
|
||||
$request->all()
|
||||
);
|
||||
$data = $this->logic->accessRole($where, $requestDeptId);
|
||||
return $this->success($data);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -182,7 +182,6 @@ class SystemUserController extends BaseController
|
||||
{
|
||||
$data = $request->post();
|
||||
unset($data['deptList']);
|
||||
unset($data['postList']);
|
||||
unset($data['roleList']);
|
||||
$result = $this->logic->updateInfo($this->adminId, $data);
|
||||
if ($result) {
|
||||
|
||||
@@ -9,6 +9,7 @@ namespace plugin\saiadmin\app\event;
|
||||
use plugin\saiadmin\app\cache\ReflectionCache;
|
||||
use plugin\saiadmin\app\model\system\SystemLoginLog;
|
||||
use plugin\saiadmin\app\model\system\SystemOperLog;
|
||||
use plugin\saiadmin\app\model\system\SystemUser as SystemUserModel;
|
||||
|
||||
class SystemUser
|
||||
{
|
||||
@@ -32,6 +33,10 @@ class SystemUser
|
||||
if (isset($item['admin_id'])) {
|
||||
$data['created_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);
|
||||
}
|
||||
@@ -49,6 +54,9 @@ class SystemUser
|
||||
return false;
|
||||
}
|
||||
$info = getCurrentInfo();
|
||||
if (!$info) {
|
||||
return false;
|
||||
}
|
||||
$ip = $request->getRealIp();
|
||||
$module = $request->plugin;
|
||||
$rule = trim($request->uri());
|
||||
@@ -60,6 +68,14 @@ class SystemUser
|
||||
$data['ip'] = $ip;
|
||||
$data['ip_location'] = self::getIpLocation($ip);
|
||||
$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);
|
||||
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)),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -2,129 +2,123 @@
|
||||
// +----------------------------------------------------------------------
|
||||
// | saiadmin [ saiadmin快速开发框架 ]
|
||||
// +----------------------------------------------------------------------
|
||||
// | Author: sai <1430792918@qq.com>
|
||||
// +----------------------------------------------------------------------
|
||||
namespace plugin\saiadmin\app\logic\system;
|
||||
|
||||
use app\dice\service\DiceChannelConfigService;
|
||||
use plugin\saiadmin\app\service\SystemRoleChannelService;
|
||||
use plugin\saiadmin\basic\think\BaseLogic;
|
||||
use plugin\saiadmin\exception\ApiException;
|
||||
use plugin\saiadmin\app\model\system\SystemDept;
|
||||
use plugin\saiadmin\app\model\system\SystemUser;
|
||||
use plugin\saiadmin\utils\Helper;
|
||||
use plugin\saiadmin\utils\Arr;
|
||||
|
||||
/**
|
||||
* 部门逻辑层
|
||||
* 渠道逻辑层(表 sa_system_dept)
|
||||
*/
|
||||
class SystemDeptLogic extends BaseLogic
|
||||
{
|
||||
/**
|
||||
* 构造函数
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->model = new SystemDept();
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加数据
|
||||
*/
|
||||
public function add($data): mixed
|
||||
{
|
||||
$data = $this->handleData($data);
|
||||
$this->model->save($data);
|
||||
return $this->model->getKey();
|
||||
$deptId = (int) $this->model->getKey();
|
||||
if ($deptId > 0) {
|
||||
(new DiceChannelConfigService())->copyDefaultConfigToDept($deptId);
|
||||
(new SystemRoleChannelService())->copyDefaultRolesToDept($deptId, false);
|
||||
}
|
||||
return $deptId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改数据
|
||||
*/
|
||||
public function edit($id, $data): mixed
|
||||
{
|
||||
$oldLevel = $data['level'] . $id . ',';
|
||||
$data = $this->handleData($data);
|
||||
if ($data['parent_id'] == $id) {
|
||||
throw new ApiException('Parent department cannot be the same as current department');
|
||||
}
|
||||
if (in_array($id, explode(',', $data['level']))) {
|
||||
throw new ApiException('Cannot set parent department to a child of current department');
|
||||
}
|
||||
$newLevel = $data['level'] . $id . ',';
|
||||
$deptIds = $this->model->where('level', 'like', $oldLevel . '%')->column('id');
|
||||
|
||||
return $this->transaction(function () use ($deptIds, $oldLevel, $newLevel, $data, $id) {
|
||||
$this->model->whereIn('id', $deptIds)->exp('level', "REPLACE(level, '$oldLevel', '$newLevel')")->update([]);
|
||||
return $this->model->update($data, ['id' => $id]);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 数据删除
|
||||
*/
|
||||
public function destroy($ids): bool
|
||||
{
|
||||
$num = $this->model->where('parent_id', 'in', $ids)->count();
|
||||
if ($num > 0) {
|
||||
throw new ApiException('This department has sub-departments, please delete them first');
|
||||
} else {
|
||||
$count = SystemUser::where('dept_id', 'in', $ids)->count();
|
||||
if ($count > 0) {
|
||||
throw new ApiException('This department has users, please delete or transfer them first');
|
||||
throw new ApiException('This channel has users, please delete or transfer them first');
|
||||
}
|
||||
return $this->model->destroy($ids);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 数据处理
|
||||
* 带关联选项删除渠道
|
||||
*/
|
||||
public function destroyWithRelations(int $deptId, array $deleteTables): bool
|
||||
{
|
||||
(new SystemRoleChannelService())->deleteRolesByDept($deptId);
|
||||
(new DiceChannelConfigService())->destroyDeptWithRelations($deptId, $deleteTables);
|
||||
return true;
|
||||
}
|
||||
|
||||
public function getDestroyPreview(array $deptIds): array
|
||||
{
|
||||
return (new DiceChannelConfigService())->getDestroyPreview($deptIds);
|
||||
}
|
||||
|
||||
public function syncAllChannelConfigs(): array
|
||||
{
|
||||
$config = (new DiceChannelConfigService())->syncAllChannelsFromDefault();
|
||||
$roles = (new SystemRoleChannelService())->syncAllChannelsFromDefault();
|
||||
return ['config' => $config, 'roles' => $roles];
|
||||
}
|
||||
|
||||
protected function handleData($data)
|
||||
{
|
||||
// 处理上级部门
|
||||
if (empty($data['parent_id']) || $data['parent_id'] == 0) {
|
||||
$data['level'] = '0';
|
||||
$data['parent_id'] = 0;
|
||||
} else {
|
||||
$parentMenu = SystemDept::findOrEmpty($data['parent_id']);
|
||||
$data['level'] = $parentMenu['level'] . $parentMenu['id'] . ',';
|
||||
}
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* 数据树形化
|
||||
* @param array $where
|
||||
* @return array
|
||||
*/
|
||||
public function tree(array $where = []): array
|
||||
{
|
||||
$query = $this->search($where);
|
||||
$request = request();
|
||||
if ($request && $request->input('tree', 'false') === 'true') {
|
||||
$query->field('id, id as value, name as label, parent_id');
|
||||
}
|
||||
$query->order('sort', 'desc');
|
||||
$query->with(['leader']);
|
||||
$data = $this->getAll($query);
|
||||
return Helper::makeTree($data);
|
||||
return $this->getAll($query);
|
||||
}
|
||||
|
||||
/**
|
||||
* 可操作部门
|
||||
* @param array $where
|
||||
* @return array
|
||||
*/
|
||||
public function accessDept(array $where = []): array
|
||||
{
|
||||
$query = $this->search($where);
|
||||
// 超级管理员(id=1)可查看全部部门,普通管理员按部门权限过滤
|
||||
if (isset($this->adminInfo['id']) && $this->adminInfo['id'] > 1) {
|
||||
$query->auth($this->adminInfo['deptList'] ?? []);
|
||||
$deptId = $this->resolveAccessibleDeptId();
|
||||
if ($deptId > 0) {
|
||||
$query->where('id', $deptId);
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
$query->field('id, id as value, name as label, parent_id');
|
||||
}
|
||||
$query->field('id, id as value, name as label');
|
||||
$query->order('sort', 'desc');
|
||||
$data = $this->getAll($query);
|
||||
return Helper::makeTree($data);
|
||||
return $this->getAll($query);
|
||||
}
|
||||
|
||||
/**
|
||||
* 当前管理员可操作的渠道 ID(deptList 缺失时回退 dept_id)
|
||||
*/
|
||||
public function resolveAccessibleDeptId(?array $adminInfo = null): int
|
||||
{
|
||||
$adminInfo = $adminInfo ?? $this->adminInfo ?? [];
|
||||
if (empty($adminInfo['id']) || (int) $adminInfo['id'] <= 1) {
|
||||
return 0;
|
||||
}
|
||||
$deptList = $adminInfo['deptList'] ?? [];
|
||||
if (is_array($deptList) && isset($deptList['id']) && (int) $deptList['id'] > 0) {
|
||||
return (int) $deptList['id'];
|
||||
}
|
||||
$deptId = $adminInfo['dept_id'] ?? null;
|
||||
if ($deptId !== null && $deptId !== '' && (int) $deptId > 0) {
|
||||
return (int) $deptId;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,95 +0,0 @@
|
||||
<?php
|
||||
// +----------------------------------------------------------------------
|
||||
// | saiadmin [ saiadmin快速开发框架 ]
|
||||
// +----------------------------------------------------------------------
|
||||
// | Author: sai <1430792918@qq.com>
|
||||
// +----------------------------------------------------------------------
|
||||
namespace plugin\saiadmin\app\logic\system;
|
||||
|
||||
use plugin\saiadmin\app\model\system\SystemPost;
|
||||
use plugin\saiadmin\basic\think\BaseLogic;
|
||||
use plugin\saiadmin\exception\ApiException;
|
||||
use plugin\saiadmin\service\OpenSpoutWriter;
|
||||
use OpenSpout\Reader\XLSX\Reader;
|
||||
|
||||
/**
|
||||
* 岗位管理逻辑层
|
||||
*/
|
||||
class SystemPostLogic extends BaseLogic
|
||||
{
|
||||
/**
|
||||
* 构造函数
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->model = new SystemPost();
|
||||
}
|
||||
|
||||
/**
|
||||
* 可操作岗位
|
||||
* @param array $where
|
||||
* @return array
|
||||
*/
|
||||
public function accessPost(array $where = []): array
|
||||
{
|
||||
$query = $this->search($where);
|
||||
$query->field('id, id as value, name as label, name, code');
|
||||
return $this->getAll($query);
|
||||
}
|
||||
|
||||
/**
|
||||
* 导入数据
|
||||
*/
|
||||
public function import($file)
|
||||
{
|
||||
$path = $this->getImport($file);
|
||||
$reader = new Reader();
|
||||
try {
|
||||
$reader->open($path);
|
||||
$data = [];
|
||||
foreach ($reader->getSheetIterator() as $sheet) {
|
||||
$isHeader = true;
|
||||
foreach ($sheet->getRowIterator() as $row) {
|
||||
if ($isHeader) {
|
||||
$isHeader = false;
|
||||
continue;
|
||||
}
|
||||
$cells = $row->getCells();
|
||||
$data[] = [
|
||||
'name' => $cells[0]->getValue(),
|
||||
'code' => $cells[1]->getValue(),
|
||||
'sort' => $cells[2]->getValue(),
|
||||
'status' => $cells[3]->getValue(),
|
||||
];
|
||||
}
|
||||
}
|
||||
$this->saveAll($data);
|
||||
} catch (\Exception $e) {
|
||||
throw new ApiException('Import file error, please upload correct xlsx file');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出数据
|
||||
*/
|
||||
public function export($where = [])
|
||||
{
|
||||
$query = $this->search($where)->field('id,name,code,sort,status,create_time');
|
||||
$data = $this->getAll($query);
|
||||
$file_name = '岗位数据.xlsx';
|
||||
$header = ['编号', '岗位名称', '岗位标识', '排序', '状态', '创建时间'];
|
||||
$filter = [
|
||||
'status' => [
|
||||
['value' => 1, 'label' => '正常'],
|
||||
['value' => 2, 'label' => '禁用']
|
||||
]
|
||||
];
|
||||
$writer = new OpenSpoutWriter($file_name);
|
||||
$writer->setWidth([15, 15, 20, 15, 15, 25]);
|
||||
$writer->setHeader($header);
|
||||
$writer->setData($data, null, $filter);
|
||||
$file_path = $writer->returnFile();
|
||||
return response()->download($file_path, urlencode($file_name));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -6,121 +6,136 @@
|
||||
// +----------------------------------------------------------------------
|
||||
namespace plugin\saiadmin\app\logic\system;
|
||||
|
||||
use app\dice\helper\AdminScopeHelper;
|
||||
use plugin\saiadmin\app\cache\UserMenuCache;
|
||||
use plugin\saiadmin\app\model\system\SystemRole;
|
||||
use plugin\saiadmin\app\service\SystemRoleChannelService;
|
||||
use plugin\saiadmin\basic\think\BaseLogic;
|
||||
use plugin\saiadmin\exception\ApiException;
|
||||
use plugin\saiadmin\utils\Helper;
|
||||
use support\think\Cache;
|
||||
use support\think\Db;
|
||||
|
||||
/**
|
||||
* 角色逻辑层
|
||||
* 角色逻辑层(按渠道 dept_id 隔离)
|
||||
*/
|
||||
class SystemRoleLogic extends BaseLogic
|
||||
{
|
||||
/**
|
||||
* 构造函数
|
||||
*/
|
||||
protected string $orderField = 'level';
|
||||
|
||||
protected string $orderType = 'desc';
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->model = new SystemRole();
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加数据
|
||||
* 分页列表(按渠道过滤)
|
||||
*/
|
||||
public function indexList(array $where, $requestDeptId = null): array
|
||||
{
|
||||
$query = $this->search($where);
|
||||
$this->applyDeptScope($query, $requestDeptId);
|
||||
$levelArr = array_column($this->adminInfo['roleList'] ?? [], 'level');
|
||||
if (!empty($levelArr)) {
|
||||
$maxLevel = max($levelArr);
|
||||
$query->where('level', '<', $maxLevel);
|
||||
}
|
||||
$query->where('id', '<>', SystemRoleChannelService::SUPER_ADMIN_ROLE_ID);
|
||||
return $this->getList($query);
|
||||
}
|
||||
|
||||
public function add($data): bool
|
||||
{
|
||||
$data = $this->handleData($data);
|
||||
$deptId = AdminScopeHelper::normalizeRecordDeptId($data['dept_id'] ?? null);
|
||||
$data['dept_id'] = $deptId;
|
||||
$this->assertCodeUniqueInDept($data['code'] ?? '', $deptId, null);
|
||||
return $this->model->save($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改数据
|
||||
*/
|
||||
public function edit($id, $data): bool
|
||||
{
|
||||
$model = $this->model->findOrEmpty($id);
|
||||
if ($model->isEmpty()) {
|
||||
throw new ApiException('Data not found');
|
||||
}
|
||||
$this->assertRoleWritable($model);
|
||||
$data = $this->handleData($data);
|
||||
$deptId = AdminScopeHelper::normalizeRecordDeptId($model->dept_id ?? $data['dept_id'] ?? null);
|
||||
$data['dept_id'] = $deptId;
|
||||
$this->assertCodeUniqueInDept($data['code'] ?? '', $deptId, (int) $id);
|
||||
return $model->save($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除数据
|
||||
*/
|
||||
public function destroy($ids): bool
|
||||
{
|
||||
// 越权保护
|
||||
$levelArr = array_column($this->adminInfo['roleList'], 'level');
|
||||
$maxLevel = max($levelArr);
|
||||
$levelArr = array_column($this->adminInfo['roleList'] ?? [], 'level');
|
||||
$maxLevel = !empty($levelArr) ? max($levelArr) : 100;
|
||||
|
||||
$num = SystemRole::where('level', '>=', $maxLevel)->whereIn('id', $ids)->count();
|
||||
if ($num > 0) {
|
||||
$idList = is_array($ids) ? $ids : explode(',', (string) $ids);
|
||||
foreach ($idList as $roleId) {
|
||||
$roleId = (int) $roleId;
|
||||
if ($roleId === SystemRoleChannelService::SUPER_ADMIN_ROLE_ID) {
|
||||
throw new ApiException('Cannot delete super admin role');
|
||||
}
|
||||
$role = $this->model->find($roleId);
|
||||
if (!$role) {
|
||||
continue;
|
||||
}
|
||||
$this->assertRoleWritable($role);
|
||||
if ((int) ($role->level ?? 0) >= $maxLevel) {
|
||||
throw new ApiException('Cannot operate roles with higher level than current account');
|
||||
} else {
|
||||
}
|
||||
}
|
||||
|
||||
return $this->model->destroy($ids);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 数据处理
|
||||
*/
|
||||
protected function handleData($data)
|
||||
{
|
||||
// 越权保护
|
||||
$levelArr = array_column($this->adminInfo['roleList'], 'level');
|
||||
$levelArr = array_column($this->adminInfo['roleList'] ?? [], 'level');
|
||||
if (!empty($levelArr)) {
|
||||
$maxLevel = max($levelArr);
|
||||
if ($data['level'] >= $maxLevel) {
|
||||
if (($data['level'] ?? 0) >= $maxLevel) {
|
||||
throw new ApiException('Cannot operate roles with higher level than current account');
|
||||
}
|
||||
}
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* 可操作角色
|
||||
* @param array $where
|
||||
* @return array
|
||||
*/
|
||||
public function accessRole(array $where = []): array
|
||||
public function accessRole(array $where = [], $requestDeptId = null): array
|
||||
{
|
||||
$query = $this->search($where);
|
||||
// 越权保护
|
||||
$levelArr = array_column($this->adminInfo['roleList'], 'level');
|
||||
$this->applyDeptScope($query, $requestDeptId);
|
||||
$levelArr = array_column($this->adminInfo['roleList'] ?? [], 'level');
|
||||
if (!empty($levelArr)) {
|
||||
$maxLevel = max($levelArr);
|
||||
$query->where('level', '<', $maxLevel);
|
||||
$query->order('sort', 'desc');
|
||||
}
|
||||
$query->where('id', '<>', SystemRoleChannelService::SUPER_ADMIN_ROLE_ID);
|
||||
return $this->getAll($query);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据角色数组获取菜单
|
||||
* @param $ids
|
||||
* @return array
|
||||
*/
|
||||
public function getMenuIdsByRoleIds($ids): array
|
||||
{
|
||||
if (empty($ids))
|
||||
if (empty($ids)) {
|
||||
return [];
|
||||
}
|
||||
return $this->model->where('id', 'in', $ids)->with([
|
||||
'menus' => function ($query) {
|
||||
$query->where('status', 1)->order('sort', 'desc');
|
||||
}
|
||||
])->select()->toArray();
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据角色获取菜单
|
||||
* @param $id
|
||||
* @return array
|
||||
*/
|
||||
public function getMenuByRole($id): array
|
||||
{
|
||||
$role = $this->model->findOrEmpty($id);
|
||||
if ($role->isEmpty()) {
|
||||
throw new ApiException('Data not found');
|
||||
}
|
||||
$this->assertRoleWritable($role);
|
||||
$menus = $role->menus ?: [];
|
||||
return [
|
||||
'id' => $id,
|
||||
@@ -128,14 +143,14 @@ class SystemRoleLogic extends BaseLogic
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存菜单权限
|
||||
* @param $id
|
||||
* @param $menu_ids
|
||||
* @return mixed
|
||||
*/
|
||||
public function saveMenuPermission($id, $menu_ids): mixed
|
||||
{
|
||||
$role = $this->model->findOrEmpty($id);
|
||||
if ($role->isEmpty()) {
|
||||
throw new ApiException('Data not found');
|
||||
}
|
||||
$this->assertRoleWritable($role);
|
||||
|
||||
return $this->transaction(function () use ($id, $menu_ids) {
|
||||
$role = $this->model->findOrEmpty($id);
|
||||
if ($role) {
|
||||
@@ -147,10 +162,90 @@ class SystemRoleLogic extends BaseLogic
|
||||
}
|
||||
$cache = config('plugin.saiadmin.saithink.button_cache');
|
||||
$tag = $cache['role'] . $id;
|
||||
Cache::tag($tag)->clear(); // 清理权限缓存-角色TAG
|
||||
UserMenuCache::clearMenuCache(); // 清理菜单缓存
|
||||
Cache::tag($tag)->clear();
|
||||
UserMenuCache::clearMenuCache();
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析并校验当前请求应操作的渠道 ID
|
||||
*/
|
||||
public function resolveRequestDeptId($requestDeptId): int
|
||||
{
|
||||
if ((int) ($this->adminInfo['id'] ?? 0) === 1) {
|
||||
return AdminScopeHelper::resolveConfigDeptId($this->adminInfo, $requestDeptId);
|
||||
}
|
||||
$deptLogic = new SystemDeptLogic();
|
||||
$deptLogic->init($this->adminInfo);
|
||||
return $deptLogic->resolveAccessibleDeptId();
|
||||
}
|
||||
|
||||
/**
|
||||
* 列表/下拉按渠道过滤
|
||||
*/
|
||||
protected function applyDeptScope($query, $requestDeptId = null): void
|
||||
{
|
||||
if (!$this->tableHasDeptIdColumn()) {
|
||||
return;
|
||||
}
|
||||
if ((int) ($this->adminInfo['id'] ?? 0) === 1) {
|
||||
$deptId = AdminScopeHelper::resolveConfigDeptId($this->adminInfo, $requestDeptId);
|
||||
$query->where('dept_id', $deptId);
|
||||
return;
|
||||
}
|
||||
$deptLogic = new SystemDeptLogic();
|
||||
$deptLogic->init($this->adminInfo);
|
||||
$deptId = $deptLogic->resolveAccessibleDeptId();
|
||||
if ($deptId > 0) {
|
||||
$query->where('dept_id', $deptId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验角色属于当前可操作渠道
|
||||
*/
|
||||
public function assertRoleWritable($role): void
|
||||
{
|
||||
if (!$this->tableHasDeptIdColumn()) {
|
||||
return;
|
||||
}
|
||||
$roleDeptId = AdminScopeHelper::normalizeRecordDeptId($role->dept_id ?? null);
|
||||
if ((int) ($role->id ?? 0) === SystemRoleChannelService::SUPER_ADMIN_ROLE_ID) {
|
||||
throw new ApiException('Cannot operate super admin role');
|
||||
}
|
||||
if ((int) ($this->adminInfo['id'] ?? 0) === 1) {
|
||||
return;
|
||||
}
|
||||
$deptLogic = new SystemDeptLogic();
|
||||
$deptLogic->init($this->adminInfo);
|
||||
$scopeDeptId = $deptLogic->resolveAccessibleDeptId();
|
||||
if ($scopeDeptId > 0 && $roleDeptId !== $scopeDeptId) {
|
||||
throw new ApiException('No permission to operate this channel role');
|
||||
}
|
||||
}
|
||||
|
||||
protected function assertCodeUniqueInDept(string $code, int $deptId, ?int $excludeId): void
|
||||
{
|
||||
if ($code === '') {
|
||||
return;
|
||||
}
|
||||
$query = SystemRole::where('code', $code)->where('dept_id', $deptId);
|
||||
if ($excludeId !== null && $excludeId > 0) {
|
||||
$query->where('id', '<>', $excludeId);
|
||||
}
|
||||
if ($query->count() > 0) {
|
||||
throw new ApiException('Role code already exists in this channel');
|
||||
}
|
||||
}
|
||||
|
||||
protected function tableHasDeptIdColumn(): bool
|
||||
{
|
||||
try {
|
||||
$fields = Db::getFields((new SystemRole())->getTable());
|
||||
return isset($fields['dept_id']);
|
||||
} catch (\Throwable $e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,10 +40,17 @@ class SystemUserLogic extends BaseLogic
|
||||
{
|
||||
$query = $this->search($where);
|
||||
$query->with(['depts']);
|
||||
// 超级管理员(id=1)可查看全部用户,普通管理员按部门权限过滤
|
||||
// 超级管理员(id=1)可查看全部用户,渠道管理员按本渠道过滤
|
||||
if (isset($this->adminInfo['id']) && $this->adminInfo['id'] > 1) {
|
||||
$deptLogic = new SystemDeptLogic();
|
||||
$deptLogic->init($this->adminInfo);
|
||||
$deptId = $deptLogic->resolveAccessibleDeptId();
|
||||
if ($deptId > 0) {
|
||||
$query->where('dept_id', $deptId);
|
||||
} else {
|
||||
$query->auth($this->adminInfo['deptList'] ?? []);
|
||||
}
|
||||
}
|
||||
return $this->getList($query);
|
||||
}
|
||||
|
||||
@@ -69,8 +76,13 @@ class SystemUserLogic extends BaseLogic
|
||||
$admin = $this->model->findOrEmpty($id);
|
||||
$data = $admin->hidden(['password'])->toArray();
|
||||
$data['roleList'] = $admin->roles->toArray() ?: [];
|
||||
$data['postList'] = $admin->posts->toArray() ?: [];
|
||||
$data['deptList'] = $admin->depts ? $admin->depts->toArray() : [];
|
||||
if (empty($data['deptList']) && ! empty($admin->dept_id)) {
|
||||
$dept = SystemDept::find($admin->dept_id);
|
||||
if ($dept && ! $dept->isEmpty()) {
|
||||
$data['deptList'] = $dept->toArray();
|
||||
}
|
||||
}
|
||||
return $data;
|
||||
}
|
||||
|
||||
@@ -101,7 +113,6 @@ class SystemUserLogic extends BaseLogic
|
||||
$data['password'] = password_hash($data['password'], PASSWORD_DEFAULT);
|
||||
return $this->transaction(function () use ($data) {
|
||||
$role_ids = $data['role_ids'] ?? [];
|
||||
$post_ids = $data['post_ids'] ?? [];
|
||||
if ($this->adminInfo['id'] > 1) {
|
||||
// 部门保护
|
||||
if (!$this->deptProtect($this->adminInfo['deptList'], $data['dept_id'])) {
|
||||
@@ -114,11 +125,7 @@ class SystemUserLogic extends BaseLogic
|
||||
}
|
||||
$user = SystemUser::create($data);
|
||||
$user->roles()->detach();
|
||||
$user->posts()->detach();
|
||||
$user->roles()->saveAll($role_ids);
|
||||
if (!empty($post_ids)) {
|
||||
$user->posts()->save($post_ids);
|
||||
}
|
||||
return $user;
|
||||
});
|
||||
}
|
||||
@@ -134,7 +141,6 @@ class SystemUserLogic extends BaseLogic
|
||||
unset($data['password']);
|
||||
return $this->transaction(function () use ($data, $id) {
|
||||
$role_ids = $data['role_ids'] ?? [];
|
||||
$post_ids = $data['post_ids'] ?? [];
|
||||
// 超级管理员可修改任意用户,普通管理员仅可修改当前部门和子部门的用户
|
||||
$query = $this->model->where('id', $id);
|
||||
if (isset($this->adminInfo['id']) && $this->adminInfo['id'] > 1) {
|
||||
@@ -157,11 +163,7 @@ class SystemUserLogic extends BaseLogic
|
||||
$result = parent::edit($id, $data);
|
||||
if ($result) {
|
||||
$user->roles()->detach();
|
||||
$user->posts()->detach();
|
||||
$user->roles()->saveAll($role_ids);
|
||||
if (!empty($post_ids)) {
|
||||
$user->posts()->save($post_ids);
|
||||
}
|
||||
UserInfoCache::clearUserInfo($id);
|
||||
UserAuthCache::clearUserAuth($id);
|
||||
UserMenuCache::clearUserMenu($id);
|
||||
|
||||
@@ -9,15 +9,15 @@ namespace plugin\saiadmin\app\model\system;
|
||||
use plugin\saiadmin\basic\think\BaseModel;
|
||||
|
||||
/**
|
||||
* 部门模型
|
||||
* 渠道模型
|
||||
*
|
||||
* sa_system_dept 部门表
|
||||
* sa_system_dept 渠道表
|
||||
*
|
||||
* @property $id 编号
|
||||
* @property $parent_id 父级ID,0为根节点
|
||||
* @property $name 部门名称
|
||||
* @property $code 部门编码
|
||||
* @property $leader_id 部门负责人ID
|
||||
* @property $parent_id 父级ID(扁平渠道固定为0)
|
||||
* @property $name 渠道名称
|
||||
* @property $code 渠道编码
|
||||
* @property $leader_id 渠道负责人ID
|
||||
* @property $level 祖级列表,格式: 0,1,5,
|
||||
* @property $sort 排序,数字越小越靠前
|
||||
* @property $status 状态: 1启用, 0禁用
|
||||
@@ -38,24 +38,21 @@ class SystemDept extends BaseModel
|
||||
protected $table = 'sa_system_dept';
|
||||
|
||||
/**
|
||||
* 权限范围
|
||||
* 权限范围(扁平渠道,仅本渠道)
|
||||
*/
|
||||
public function scopeAuth($query, $value)
|
||||
{
|
||||
if (!empty($value) && isset($value['id'])) {
|
||||
$deptIds = [$value['id']];
|
||||
$level = $value['level'] ?? '';
|
||||
if ($level !== '' && $level !== null) {
|
||||
$deptLevel = $level . $value['id'] . ',';
|
||||
$ids = static::whereLike('level', $deptLevel . '%')->column('id');
|
||||
$deptIds = array_merge($deptIds, $ids);
|
||||
if (is_array($value) && isset($value['id']) && (int) $value['id'] > 0) {
|
||||
$query->where('id', $value['id']);
|
||||
return;
|
||||
}
|
||||
$query->whereIn('id', $deptIds);
|
||||
if (is_numeric($value) && (int) $value > 0) {
|
||||
$query->where('id', (int) $value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 部门领导
|
||||
* 渠道负责人
|
||||
*/
|
||||
public function leader()
|
||||
{
|
||||
|
||||
@@ -23,6 +23,7 @@ use plugin\saiadmin\basic\think\BaseModel;
|
||||
* @property $status 登录状态
|
||||
* @property $message 提示消息
|
||||
* @property $login_time 登录时间
|
||||
* @property $dept_id 所属渠道
|
||||
* @property $remark 备注
|
||||
* @property $created_by 创建者
|
||||
* @property $updated_by 更新者
|
||||
@@ -47,4 +48,14 @@ class SystemLoginLog extends BaseModel
|
||||
$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_location IP所属地
|
||||
* @property $request_data 请求数据
|
||||
* @property $dept_id 所属渠道
|
||||
* @property $remark 备注
|
||||
* @property $created_by 创建者
|
||||
* @property $updated_by 更新者
|
||||
@@ -39,4 +40,14 @@ class SystemOperLog extends BaseModel
|
||||
|
||||
protected $table = 'sa_system_oper_log';
|
||||
|
||||
/**
|
||||
* 渠道搜索
|
||||
*/
|
||||
public function searchDeptIdAttr($query, $value): void
|
||||
{
|
||||
if ($value !== '' && $value !== null && $value > 0) {
|
||||
$query->where('dept_id', '=', $value);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
<?php
|
||||
// +----------------------------------------------------------------------
|
||||
// | saiadmin [ saiadmin快速开发框架 ]
|
||||
// +----------------------------------------------------------------------
|
||||
// | Author: sai <1430792918@qq.com>
|
||||
// +----------------------------------------------------------------------
|
||||
namespace plugin\saiadmin\app\model\system;
|
||||
|
||||
use plugin\saiadmin\basic\think\BaseModel;
|
||||
|
||||
/**
|
||||
* 岗位模型
|
||||
*
|
||||
* sa_system_post 岗位信息表
|
||||
*
|
||||
* @property $id 主键
|
||||
* @property $name 岗位名称
|
||||
* @property $code 岗位代码
|
||||
* @property $sort 排序
|
||||
* @property $status 状态
|
||||
* @property $remark 备注
|
||||
* @property $created_by 创建者
|
||||
* @property $updated_by 更新者
|
||||
* @property $create_time 创建时间
|
||||
* @property $update_time 修改时间
|
||||
*/
|
||||
class SystemPost extends BaseModel
|
||||
{
|
||||
/**
|
||||
* 数据表主键
|
||||
* @var string
|
||||
*/
|
||||
protected $pk = 'id';
|
||||
|
||||
protected $table = 'sa_system_post';
|
||||
|
||||
}
|
||||
@@ -14,6 +14,7 @@ use plugin\saiadmin\basic\think\BaseModel;
|
||||
* sa_system_role 角色表
|
||||
*
|
||||
* @property $id
|
||||
* @property int $dept_id 所属渠道ID,0=默认模板
|
||||
* @property $name 角色名称
|
||||
* @property $code 角色标识,如: hr_manager
|
||||
* @property $level 角色级别:用于行政控制,不可操作级别大于自己的角色
|
||||
@@ -41,6 +42,14 @@ class SystemRole extends BaseModel
|
||||
*/
|
||||
protected $table = 'sa_system_role';
|
||||
|
||||
/** 按渠道筛选 */
|
||||
public function searchDeptIdAttr($query, $value): void
|
||||
{
|
||||
if ($value !== '' && $value !== null) {
|
||||
$query->where('dept_id', '=', $value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 权限范围
|
||||
*/
|
||||
|
||||
@@ -24,7 +24,7 @@ use plugin\saiadmin\basic\think\BaseModel;
|
||||
* @property $phone 手机号
|
||||
* @property $signed 个性签名
|
||||
* @property $dashboard 工作台
|
||||
* @property $dept_id 主归属部门
|
||||
* @property $dept_id 主归属渠道
|
||||
* @property $is_super 是否超级管理员: 1是
|
||||
* @property $status 状态: 1启用, 2禁用
|
||||
* @property $remark 备注
|
||||
@@ -82,16 +82,12 @@ class SystemUser extends BaseModel
|
||||
}
|
||||
|
||||
/**
|
||||
* 权限范围 - 过滤部门用户
|
||||
* 权限范围 - 过滤同渠道用户
|
||||
*/
|
||||
public function scopeAuth($query, $value)
|
||||
{
|
||||
if (!empty($value)) {
|
||||
$deptIds = [$value['id']];
|
||||
$deptLevel = $value['level'] . $value['id'] . ',';
|
||||
$dept_ids = SystemDept::whereLike('level', $deptLevel . '%')->column('id');
|
||||
$deptIds = array_merge($deptIds, $dept_ids);
|
||||
$query->whereIn('dept_id', $deptIds);
|
||||
if (!empty($value) && isset($value['id'])) {
|
||||
$query->where('dept_id', $value['id']);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,15 +100,7 @@ class SystemUser extends BaseModel
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过中间表关联岗位
|
||||
*/
|
||||
public function posts()
|
||||
{
|
||||
return $this->belongsToMany(SystemPost::class, SystemUserPost::class, 'post_id', 'user_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过中间表关联部门
|
||||
* 关联渠道
|
||||
*/
|
||||
public function depts()
|
||||
{
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
<?php
|
||||
// +----------------------------------------------------------------------
|
||||
// | saiadmin [ saiadmin快速开发框架 ]
|
||||
// +----------------------------------------------------------------------
|
||||
// | Author: sai <1430792918@qq.com>
|
||||
// +----------------------------------------------------------------------
|
||||
namespace plugin\saiadmin\app\model\system;
|
||||
|
||||
use think\model\Pivot;
|
||||
|
||||
/**
|
||||
* 用户岗位关联模型
|
||||
*
|
||||
* sa_system_user_post 用户与岗位关联表
|
||||
*
|
||||
* @property $id 主键
|
||||
* @property $user_id 用户主键
|
||||
* @property $post_id 岗位主键
|
||||
*/
|
||||
class SystemUserPost extends Pivot
|
||||
{
|
||||
protected $pk = 'id';
|
||||
|
||||
protected $table = 'sa_system_user_post';
|
||||
}
|
||||
279
server/plugin/saiadmin/app/service/SystemRoleChannelService.php
Normal file
@@ -0,0 +1,279 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace plugin\saiadmin\app\service;
|
||||
|
||||
use plugin\saiadmin\app\model\system\SystemDept;
|
||||
use plugin\saiadmin\app\model\system\SystemRole;
|
||||
use plugin\saiadmin\exception\ApiException;
|
||||
use support\think\Db;
|
||||
|
||||
/**
|
||||
* 渠道角色:从默认模板复制指定角色、同步与删除
|
||||
*/
|
||||
class SystemRoleChannelService
|
||||
{
|
||||
/** 全局超级管理员角色,不参与渠道复制 */
|
||||
public const SUPER_ADMIN_ROLE_ID = 1;
|
||||
|
||||
/**
|
||||
* 为渠道从默认模板复制三个代理角色(缺失则补齐,不整包跳过)
|
||||
*/
|
||||
public function copyDefaultRolesToDept(int $deptId, bool $pruneExtra = false): array
|
||||
{
|
||||
if ($deptId <= 0) {
|
||||
throw new ApiException('Invalid channel id');
|
||||
}
|
||||
if (!$this->tableHasColumn('sa_system_role', 'dept_id')) {
|
||||
return ['dept_id' => $deptId, 'copied' => 0, 'skipped' => 0, 'pruned' => 0, 'message' => 'dept_id column missing'];
|
||||
}
|
||||
|
||||
$templates = $this->defaultTemplateRoles();
|
||||
if (empty($templates)) {
|
||||
return ['dept_id' => $deptId, 'copied' => 0, 'skipped' => 0, 'pruned' => 0, 'message' => 'no template roles'];
|
||||
}
|
||||
|
||||
$copied = 0;
|
||||
$skipped = 0;
|
||||
foreach ($templates as $template) {
|
||||
$template = (array) $template;
|
||||
$templateId = (int) ($template['id'] ?? 0);
|
||||
$code = (string) ($template['code'] ?? '');
|
||||
if ($templateId <= 0 || $code === '') {
|
||||
continue;
|
||||
}
|
||||
if ($this->roleExists($deptId, $code)) {
|
||||
$skipped++;
|
||||
continue;
|
||||
}
|
||||
|
||||
$newId = $this->insertRoleFromTemplate($template, $deptId);
|
||||
if ($newId > 0) {
|
||||
$this->copyRoleMenus($templateId, $newId);
|
||||
$copied++;
|
||||
}
|
||||
}
|
||||
|
||||
$pruned = 0;
|
||||
if ($pruneExtra) {
|
||||
$pruned = $this->pruneExtraChannelRoles($deptId);
|
||||
}
|
||||
|
||||
return ['dept_id' => $deptId, 'copied' => $copied, 'skipped' => $skipped, 'pruned' => $pruned];
|
||||
}
|
||||
|
||||
/**
|
||||
* 为所有已启用渠道补齐三个默认角色,并移除多余历史角色
|
||||
*/
|
||||
public function syncAllChannelsFromDefault(): array
|
||||
{
|
||||
$deptIds = SystemDept::where('status', 1)->where('id', '>', 0)->column('id');
|
||||
$result = [];
|
||||
foreach ($deptIds as $deptId) {
|
||||
$result[(int) $deptId] = $this->copyDefaultRolesToDept((int) $deptId, true);
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除渠道下全部角色及菜单关联
|
||||
*/
|
||||
public function deleteRolesByDept(int $deptId): int
|
||||
{
|
||||
if ($deptId <= 0) {
|
||||
return 0;
|
||||
}
|
||||
$roleIds = SystemRole::where('dept_id', $deptId)->column('id');
|
||||
if (empty($roleIds)) {
|
||||
return 0;
|
||||
}
|
||||
Db::name('sa_system_user_role')->whereIn('role_id', $roleIds)->delete();
|
||||
Db::name('sa_system_role_menu')->whereIn('role_id', $roleIds)->delete();
|
||||
Db::name('sa_system_role_dept')->whereIn('role_id', $roleIds)->delete();
|
||||
return SystemRole::destroy($roleIds);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将用户已绑定的模板角色映射到其渠道对应角色(仅三个默认 code)
|
||||
*/
|
||||
public function remapUserRolesToChannelRoles(): int
|
||||
{
|
||||
if (!$this->tableHasColumn('sa_system_role', 'dept_id')) {
|
||||
return 0;
|
||||
}
|
||||
$codes = $this->getDefaultChannelRoleCodes();
|
||||
if (empty($codes)) {
|
||||
return 0;
|
||||
}
|
||||
$codeList = "'" . implode("','", array_map('addslashes', $codes)) . "'";
|
||||
return Db::execute(
|
||||
'UPDATE `sa_system_user_role` ur
|
||||
INNER JOIN `sa_system_user` u ON ur.user_id = u.id
|
||||
INNER JOIN `sa_system_role` r_old ON ur.role_id = r_old.id
|
||||
INNER JOIN `sa_system_role` r_new ON r_new.dept_id = u.dept_id AND r_new.code = r_old.code
|
||||
SET ur.role_id = r_new.id
|
||||
WHERE u.dept_id > 0
|
||||
AND r_old.dept_id = 0
|
||||
AND r_old.code IN (' . $codeList . ')
|
||||
AND r_old.id <> ' . self::SUPER_ADMIN_ROLE_ID
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
public function getDefaultChannelRoleCodes(): array
|
||||
{
|
||||
$codes = config('plugin.saiadmin.saithink.channel_default_role_codes', []);
|
||||
if (!is_array($codes) || $codes === []) {
|
||||
return ['yijidaili', 'erjidaili', 'sanjidaili'];
|
||||
}
|
||||
$out = [];
|
||||
foreach ($codes as $code) {
|
||||
$code = trim((string) $code);
|
||||
if ($code !== '') {
|
||||
$out[] = $code;
|
||||
}
|
||||
}
|
||||
return $out;
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除渠道下不在默认三个 code 内、且未被用户绑定的角色
|
||||
*/
|
||||
public function pruneExtraChannelRoles(int $deptId): int
|
||||
{
|
||||
if ($deptId <= 0) {
|
||||
return 0;
|
||||
}
|
||||
$allowed = $this->getDefaultChannelRoleCodes();
|
||||
if (empty($allowed)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$query = SystemRole::where('dept_id', $deptId)->whereNotIn('code', $allowed);
|
||||
$roleIds = $query->column('id');
|
||||
if (empty($roleIds)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$usedIds = Db::name('sa_system_user_role')->whereIn('role_id', $roleIds)->column('role_id');
|
||||
$usedMap = array_flip($usedIds ?: []);
|
||||
$pruned = 0;
|
||||
foreach ($roleIds as $roleId) {
|
||||
if (isset($usedMap[$roleId])) {
|
||||
continue;
|
||||
}
|
||||
Db::name('sa_system_role_menu')->where('role_id', $roleId)->delete();
|
||||
Db::name('sa_system_role_dept')->where('role_id', $roleId)->delete();
|
||||
SystemRole::destroy($roleId);
|
||||
$pruned++;
|
||||
}
|
||||
|
||||
return $pruned;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, array<string, mixed>>
|
||||
*/
|
||||
private function defaultTemplateRoles(): array
|
||||
{
|
||||
$codes = $this->getDefaultChannelRoleCodes();
|
||||
if (empty($codes)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$query = Db::table('sa_system_role')
|
||||
->where('id', '<>', self::SUPER_ADMIN_ROLE_ID)
|
||||
->whereIn('code', $codes);
|
||||
if ($this->tableHasColumn('sa_system_role', 'dept_id')) {
|
||||
$query->where('dept_id', 0);
|
||||
}
|
||||
$rows = $query->select()->toArray();
|
||||
if (empty($rows)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (count($rows) === count($codes)) {
|
||||
return $this->sortRolesByLevelDesc($rows);
|
||||
}
|
||||
|
||||
// 按配置 code 补齐,缺失的 code 跳过,最终按角色级别从大到小排序
|
||||
$byCode = [];
|
||||
foreach ($rows as $row) {
|
||||
$byCode[(string) ($row['code'] ?? '')] = $row;
|
||||
}
|
||||
$ordered = [];
|
||||
foreach ($codes as $code) {
|
||||
if (isset($byCode[$code])) {
|
||||
$ordered[] = $byCode[$code];
|
||||
}
|
||||
}
|
||||
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
|
||||
{
|
||||
unset(
|
||||
$template['id'],
|
||||
$template['create_time'],
|
||||
$template['update_time'],
|
||||
$template['delete_time']
|
||||
);
|
||||
$template['dept_id'] = $deptId;
|
||||
$now = date('Y-m-d H:i:s');
|
||||
if (!isset($template['create_time'])) {
|
||||
$template['create_time'] = $now;
|
||||
}
|
||||
if (!isset($template['update_time'])) {
|
||||
$template['update_time'] = $now;
|
||||
}
|
||||
return (int) Db::table('sa_system_role')->insertGetId($template);
|
||||
}
|
||||
|
||||
private function copyRoleMenus(int $fromRoleId, int $toRoleId): void
|
||||
{
|
||||
$menuIds = Db::name('sa_system_role_menu')->where('role_id', $fromRoleId)->column('menu_id');
|
||||
if (empty($menuIds)) {
|
||||
return;
|
||||
}
|
||||
$rows = [];
|
||||
foreach ($menuIds as $menuId) {
|
||||
$rows[] = ['role_id' => $toRoleId, 'menu_id' => $menuId];
|
||||
}
|
||||
Db::name('sa_system_role_menu')->limit(100)->insertAll($rows);
|
||||
}
|
||||
|
||||
private function roleExists(int $deptId, string $code): bool
|
||||
{
|
||||
return SystemRole::where('dept_id', $deptId)->where('code', $code)->count() > 0;
|
||||
}
|
||||
|
||||
private function tableHasColumn(string $table, string $column): bool
|
||||
{
|
||||
try {
|
||||
$fields = Db::getFields($table);
|
||||
return isset($fields[$column]);
|
||||
} catch (\Throwable $e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,7 @@ namespace plugin\saiadmin\app\validate\system;
|
||||
use plugin\saiadmin\basic\BaseValidate;
|
||||
|
||||
/**
|
||||
* 部门验证器
|
||||
* 渠道验证器
|
||||
*/
|
||||
class SystemDeptValidate extends BaseValidate
|
||||
{
|
||||
@@ -25,7 +25,7 @@ class SystemDeptValidate extends BaseValidate
|
||||
* 定义错误信息
|
||||
*/
|
||||
protected $message = [
|
||||
'name' => '部门名称必须填写',
|
||||
'name' => '渠道名称必须填写',
|
||||
'status' => '状态必须填写',
|
||||
];
|
||||
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
<?php
|
||||
// +----------------------------------------------------------------------
|
||||
// | saiadmin [ saiadmin快速开发框架 ]
|
||||
// +----------------------------------------------------------------------
|
||||
// | Author: sai <1430792918@qq.com>
|
||||
// +----------------------------------------------------------------------
|
||||
namespace plugin\saiadmin\app\validate\system;
|
||||
|
||||
use plugin\saiadmin\basic\BaseValidate;
|
||||
|
||||
/**
|
||||
* 用户角色验证器
|
||||
*/
|
||||
class SystemPostValidate extends BaseValidate
|
||||
{
|
||||
/**
|
||||
* 定义验证规则
|
||||
*/
|
||||
protected $rule = [
|
||||
'name' => 'require',
|
||||
'code' => 'require',
|
||||
'status' => 'require',
|
||||
];
|
||||
|
||||
/**
|
||||
* 定义错误信息
|
||||
*/
|
||||
protected $message = [
|
||||
'name' => '岗位名称必须填写',
|
||||
'code' => '岗位标识必须填写',
|
||||
'status' => '状态必须填写',
|
||||
];
|
||||
|
||||
/**
|
||||
* 定义场景
|
||||
*/
|
||||
protected $scene = [
|
||||
'save' => [
|
||||
'name',
|
||||
'code',
|
||||
'status',
|
||||
],
|
||||
'update' => [
|
||||
'name',
|
||||
'code',
|
||||
'status',
|
||||
],
|
||||
];
|
||||
|
||||
}
|
||||
@@ -19,7 +19,7 @@ class SystemRoleValidate extends BaseValidate
|
||||
*/
|
||||
protected $rule = [
|
||||
'name' => 'require|max:16',
|
||||
'code' => 'require|alphaDash|unique:' . SystemRole::class,
|
||||
'code' => 'require|alphaDash|unique:' . SystemRole::class . ',code^dept_id',
|
||||
'status' => 'require',
|
||||
];
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
// +----------------------------------------------------------------------
|
||||
namespace plugin\saiadmin\basic;
|
||||
|
||||
use app\api\util\ApiLang;
|
||||
use support\Request;
|
||||
use support\Response;
|
||||
|
||||
@@ -36,6 +37,7 @@ class OpenController
|
||||
if (is_string($data)) {
|
||||
$msg = $data;
|
||||
}
|
||||
$msg = ApiLang::translate($msg, request());
|
||||
return json(['code' => 200, 'message' => $msg, 'data' => $data], $option);
|
||||
}
|
||||
|
||||
@@ -47,6 +49,7 @@ class OpenController
|
||||
*/
|
||||
public function fail(string $msg = 'fail', int $code = 400): Response
|
||||
{
|
||||
$msg = ApiLang::translate($msg, request());
|
||||
return json(['code' => $code, 'message' => $msg]);
|
||||
}
|
||||
|
||||
|
||||
@@ -12,10 +12,18 @@ use plugin\saiadmin\basic\contracts\ModelInterface;
|
||||
|
||||
/**
|
||||
* ThinkORM 模型基类
|
||||
*
|
||||
* 全局策略:所有删除一律为硬删除(物理删除)。
|
||||
* - 保留 SoftDelete trait 仅是为了兼容历史字段(如 delete_time)与查询作用域,
|
||||
* 实际删除方法(delete/destroy)均通过 trait 别名重写为强制 force=true。
|
||||
* - 项目中不使用 withTrashed/onlyTrashed/restore() 等软删除恢复接口。
|
||||
*/
|
||||
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
|
||||
* @param Model $model
|
||||
|
||||
@@ -23,6 +23,7 @@ Route::group('/core', function () {
|
||||
Route::get('/dice/dashboard/rechargeBarChart', [\app\dice\controller\DiceDashboardController::class, 'rechargeBarChart']);
|
||||
Route::get('/dice/dashboard/walletRecordList', [\app\dice\controller\DiceDashboardController::class, 'walletRecordList']);
|
||||
Route::get('/dice/dashboard/newPlayerList', [\app\dice\controller\DiceDashboardController::class, 'newPlayerList']);
|
||||
Route::get('/dice/dashboard/playRecordList', [\app\dice\controller\DiceDashboardController::class, 'playRecordList']);
|
||||
Route::get('/system/clearAllCache', [plugin\saiadmin\app\controller\SystemController::class, 'clearAllCache']);
|
||||
|
||||
Route::get("/system/getResourceCategory", [plugin\saiadmin\app\controller\SystemController::class, 'getResourceCategory']);
|
||||
@@ -49,14 +50,11 @@ Route::group('/core', function () {
|
||||
Route::get("/role/getMenuByRole", [\plugin\saiadmin\app\controller\system\SystemRoleController::class, 'getMenuByRole']);
|
||||
Route::post("/role/menuPermission", [\plugin\saiadmin\app\controller\system\SystemRoleController::class, 'menuPermission']);
|
||||
|
||||
// 部门管理
|
||||
// 渠道管理
|
||||
fastRoute("dept", \plugin\saiadmin\app\controller\system\SystemDeptController::class);
|
||||
Route::get("/dept/accessDept", [\plugin\saiadmin\app\controller\system\SystemDeptController::class, 'accessDept']);
|
||||
|
||||
// 岗位管理
|
||||
fastRoute('post', \plugin\saiadmin\app\controller\system\SystemPostController::class);
|
||||
Route::get("/post/accessPost", [\plugin\saiadmin\app\controller\system\SystemPostController::class, 'accessPost']);
|
||||
Route::post("/post/downloadTemplate", [plugin\saiadmin\app\controller\system\SystemPostController::class, 'downloadTemplate']);
|
||||
Route::get("/dept/destroyPreview", [\plugin\saiadmin\app\controller\system\SystemDeptController::class, 'destroyPreview']);
|
||||
Route::post("/dept/syncChannelConfigs", [\plugin\saiadmin\app\controller\system\SystemDeptController::class, 'syncChannelConfigs']);
|
||||
|
||||
// 菜单管理
|
||||
fastRoute('menu', \plugin\saiadmin\app\controller\system\SystemMenuController::class);
|
||||
@@ -83,6 +81,10 @@ Route::group('/core', function () {
|
||||
Route::delete("/logs/deleteOperLog", [\plugin\saiadmin\app\controller\system\SystemLogController::class, 'deleteOperLog']);
|
||||
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/cache", [\plugin\saiadmin\app\controller\system\SystemServerController::class, 'cache']);
|
||||
@@ -94,6 +96,7 @@ Route::group('/core', function () {
|
||||
Route::put('/dice/player/DicePlayer/updateStatus', [\app\dice\controller\player\DicePlayerController::class, 'updateStatus']);
|
||||
Route::get('/dice/player/DicePlayer/getLotteryConfigOptions', [\app\dice\controller\player\DicePlayerController::class, 'getLotteryConfigOptions']);
|
||||
Route::get('/dice/player/DicePlayer/getSystemUserOptions', [\app\dice\controller\player\DicePlayerController::class, 'getSystemUserOptions']);
|
||||
Route::get('/dice/player/DicePlayer/getSystemUserTreeOptions', [\app\dice\controller\player\DicePlayerController::class, 'getSystemUserTreeOptions']);
|
||||
Route::get('/dice/player/DicePlayer/getGameUrl', [\app\dice\controller\player\DicePlayerController::class, 'getGameUrl']);
|
||||
fastRoute('dice/play_record/DicePlayRecord', \app\dice\controller\play_record\DicePlayRecordController::class);
|
||||
Route::get('/dice/play_record/DicePlayRecord/getPlayerOptions', [\app\dice\controller\play_record\DicePlayRecordController::class, 'getPlayerOptions']);
|
||||
@@ -116,10 +119,13 @@ 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/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/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/createRewardReferencePreview', [\app\dice\controller\reward_config\DiceRewardConfigController::class, 'createRewardReferencePreview']);
|
||||
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/ante_config/DiceAnteConfig', \app\dice\controller\ante_config\DiceAnteConfigController::class);
|
||||
Route::get('/dice/ante_config/DiceAnteConfig/getOptions', [\app\dice\controller\ante_config\DiceAnteConfigController::class, 'getOptions']);
|
||||
fastRoute('dice/lottery_pool_config/DiceLotteryPoolConfig', \app\dice\controller\lottery_pool_config\DiceLotteryPoolConfigController::class);
|
||||
Route::get('/dice/lottery_pool_config/DiceLotteryPoolConfig/getOptions', [\app\dice\controller\lottery_pool_config\DiceLotteryPoolConfigController::class, 'getOptions']);
|
||||
Route::get('/dice/lottery_pool_config/DiceLotteryPoolConfig/getCurrentPool', [\app\dice\controller\lottery_pool_config\DiceLotteryPoolConfigController::class, 'getCurrentPool']);
|
||||
|
||||
@@ -33,7 +33,6 @@ return [
|
||||
'expire' => 60 * 60 * 4,
|
||||
'dept' => 'saiadmin:user_cache:dept_',
|
||||
'role' => 'saiadmin:user_cache:role_',
|
||||
'post' => 'saiadmin:user_cache:post_',
|
||||
],
|
||||
|
||||
// 用户权限缓存
|
||||
@@ -73,4 +72,13 @@ return [
|
||||
'attr' => 'saiadmin:reflection_cache:attr_',
|
||||
],
|
||||
|
||||
/**
|
||||
* 新建渠道时从默认模板复制的角色 code(须存在于 dept_id=0 的模板角色)
|
||||
*/
|
||||
'channel_default_role_codes' => [
|
||||
'yijidaili',
|
||||
'erjidaili',
|
||||
'sanjidaili',
|
||||
],
|
||||
|
||||
];
|
||||
|
||||
@@ -391,7 +391,6 @@ INSERT INTO `sa_system_menu` VALUES (3, 0, '系统管理', 'System', NULL, 1, '/
|
||||
INSERT INTO `sa_system_menu` VALUES (4, 3, '用户管理', 'User', NULL, 2, 'user', '/system/user', NULL, 'ri:user-line', 100, NULL, 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
|
||||
INSERT INTO `sa_system_menu` VALUES (5, 3, '部门管理', 'Dept', NULL, 2, 'dept', '/system/dept', NULL, 'ri:node-tree', 100, NULL, 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
|
||||
INSERT INTO `sa_system_menu` VALUES (6, 3, '角色管理', 'Role', NULL, 2, 'role', '/system/role', NULL, 'ri:admin-line', 100, NULL, 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
|
||||
INSERT INTO `sa_system_menu` VALUES (7, 3, '岗位管理', 'Post', '', 2, 'post', '/system/post', NULL, 'ri:signpost-line', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
|
||||
INSERT INTO `sa_system_menu` VALUES (8, 3, '菜单管理', 'Menu', NULL, 2, 'menu', '/system/menu', NULL, 'ri:menu-line', 100, NULL, 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
|
||||
INSERT INTO `sa_system_menu` VALUES (10, 0, '运维管理', 'Safeguard', NULL, 1, '/safeguard', '', NULL, 'ri:shield-check-line', 100, NULL, 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
|
||||
INSERT INTO `sa_system_menu` VALUES (11, 10, '缓存管理', 'Cache', '', 2, 'cache', '/safeguard/cache', NULL, 'ri:keyboard-box-line', 80, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
|
||||
@@ -423,13 +422,6 @@ INSERT INTO `sa_system_menu` VALUES (36, 6, '修改', '', 'core:role:update', 3,
|
||||
INSERT INTO `sa_system_menu` VALUES (37, 6, '读取', '', 'core:role:read', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
|
||||
INSERT INTO `sa_system_menu` VALUES (38, 6, '删除', '', 'core:role:destroy', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
|
||||
INSERT INTO `sa_system_menu` VALUES (39, 6, '菜单权限', '', 'core:role:menu', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
|
||||
INSERT INTO `sa_system_menu` VALUES (41, 7, '数据列表', '', 'core:post:index', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
|
||||
INSERT INTO `sa_system_menu` VALUES (42, 7, '添加', '', 'core:post:save', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
|
||||
INSERT INTO `sa_system_menu` VALUES (43, 7, '修改', '', 'core:post:update', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
|
||||
INSERT INTO `sa_system_menu` VALUES (44, 7, '读取', '', 'core:post:read', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
|
||||
INSERT INTO `sa_system_menu` VALUES (45, 7, '删除', '', 'core:post:destroy', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
|
||||
INSERT INTO `sa_system_menu` VALUES (46, 7, '导入', '', 'core:post:import', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
|
||||
INSERT INTO `sa_system_menu` VALUES (47, 7, '导出', '', 'core:post:export', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
|
||||
INSERT INTO `sa_system_menu` VALUES (48, 8, '数据列表', '', 'core:menu:index', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
|
||||
INSERT INTO `sa_system_menu` VALUES (49, 8, '读取', '', 'core:menu:read', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
|
||||
INSERT INTO `sa_system_menu` VALUES (50, 8, '添加', '', 'core:menu:save', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
|
||||
@@ -501,31 +493,6 @@ CREATE TABLE `sa_system_oper_log` (
|
||||
-- Records of sa_system_oper_log
|
||||
-- ----------------------------
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for sa_system_post
|
||||
-- ----------------------------
|
||||
DROP TABLE IF EXISTS `sa_system_post`;
|
||||
CREATE TABLE `sa_system_post` (
|
||||
`id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键',
|
||||
`name` varchar(50) NULL DEFAULT NULL COMMENT '岗位名称',
|
||||
`code` varchar(100) NULL DEFAULT NULL COMMENT '岗位代码',
|
||||
`sort` smallint(5) UNSIGNED NULL DEFAULT 0 COMMENT '排序',
|
||||
`status` smallint(6) NULL DEFAULT 1 COMMENT '状态 (1正常 2停用)',
|
||||
`remark` varchar(255) NULL DEFAULT NULL COMMENT '备注',
|
||||
`created_by` int(11) NULL DEFAULT NULL COMMENT '创建者',
|
||||
`updated_by` int(11) NULL DEFAULT NULL COMMENT '更新者',
|
||||
`create_time` datetime(0) NULL DEFAULT NULL COMMENT '创建时间',
|
||||
`update_time` datetime(0) NULL DEFAULT NULL COMMENT '修改时间',
|
||||
`delete_time` datetime(0) NULL DEFAULT NULL COMMENT '删除时间',
|
||||
PRIMARY KEY (`id`) USING BTREE
|
||||
) ENGINE = InnoDB AUTO_INCREMENT = 87 COMMENT = '岗位信息表' ROW_FORMAT = Dynamic;
|
||||
|
||||
-- ----------------------------
|
||||
-- Records of sa_system_post
|
||||
-- ----------------------------
|
||||
INSERT INTO `sa_system_post` VALUES (1, '司机岗', 'driver', 100, 1, '', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
|
||||
INSERT INTO `sa_system_post` VALUES (2, '保安岗', 'security', 100, 1, '', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for sa_system_role
|
||||
-- ----------------------------
|
||||
@@ -635,23 +602,6 @@ INSERT INTO `sa_system_user` VALUES (10, 'timi_boss', '$2y$10$sY/4StKVV.N/8Ock8J
|
||||
INSERT INTO `sa_system_user` VALUES (100, 'dev_wang', '$2y$10$sY/4StKVV.N/8Ock8J8kdeIOK4jS4tAUoYjkzvB8Tzy0fLh.wA2KS', '王程序员', NULL, 'https://image.saithink.top/saiadmin/avatar.jpg', NULL, '15888888888', NULL, 'work', 1111, 0, 1, NULL, NULL, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
|
||||
INSERT INTO `sa_system_user` VALUES (101, 'dev_li', '$2y$10$sY/4StKVV.N/8Ock8J8kdeIOK4jS4tAUoYjkzvB8Tzy0fLh.wA2KS', '李策划', NULL, 'https://image.saithink.top/saiadmin/avatar.jpg', NULL, '15888888888', NULL, 'work', 1111, 0, 1, NULL, NULL, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for sa_system_user_post
|
||||
-- ----------------------------
|
||||
DROP TABLE IF EXISTS `sa_system_user_post`;
|
||||
CREATE TABLE `sa_system_user_post` (
|
||||
`id` bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键',
|
||||
`user_id` bigint(20) UNSIGNED NOT NULL COMMENT '用户主键',
|
||||
`post_id` bigint(20) UNSIGNED NOT NULL COMMENT '岗位主键',
|
||||
PRIMARY KEY (`id`) USING BTREE,
|
||||
INDEX `idx_user_id`(`user_id`) USING BTREE,
|
||||
INDEX `idx_post_id`(`post_id`) USING BTREE
|
||||
) ENGINE = InnoDB AUTO_INCREMENT = 1 COMMENT = '用户与岗位关联表' ROW_FORMAT = Dynamic;
|
||||
|
||||
-- ----------------------------
|
||||
-- Records of sa_system_user_post
|
||||
-- ----------------------------
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for sa_system_user_role
|
||||
-- ----------------------------
|
||||
|
||||
@@ -380,7 +380,6 @@ INSERT INTO `sa_system_menu` VALUES (3, 0, '系统管理', 'System', NULL, 1, '/
|
||||
INSERT INTO `sa_system_menu` VALUES (4, 3, '用户管理', 'User', NULL, 2, 'user', '/system/user', NULL, 'ri:user-line', 100, NULL, 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
|
||||
INSERT INTO `sa_system_menu` VALUES (5, 3, '部门管理', 'Dept', NULL, 2, 'dept', '/system/dept', NULL, 'ri:node-tree', 100, NULL, 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
|
||||
INSERT INTO `sa_system_menu` VALUES (6, 3, '角色管理', 'Role', NULL, 2, 'role', '/system/role', NULL, 'ri:admin-line', 100, NULL, 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
|
||||
INSERT INTO `sa_system_menu` VALUES (7, 3, '岗位管理', 'Post', '', 2, 'post', '/system/post', NULL, 'ri:signpost-line', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
|
||||
INSERT INTO `sa_system_menu` VALUES (8, 3, '菜单管理', 'Menu', NULL, 2, 'menu', '/system/menu', NULL, 'ri:menu-line', 100, NULL, 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
|
||||
INSERT INTO `sa_system_menu` VALUES (10, 0, '运维管理', 'Safeguard', NULL, 1, '/safeguard', '', NULL, 'ri:shield-check-line', 100, NULL, 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
|
||||
INSERT INTO `sa_system_menu` VALUES (11, 10, '缓存管理', 'Cache', '', 2, 'cache', '/safeguard/cache', NULL, 'ri:keyboard-box-line', 80, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
|
||||
@@ -412,13 +411,6 @@ INSERT INTO `sa_system_menu` VALUES (36, 6, '修改', '', 'core:role:update', 3,
|
||||
INSERT INTO `sa_system_menu` VALUES (37, 6, '读取', '', 'core:role:read', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
|
||||
INSERT INTO `sa_system_menu` VALUES (38, 6, '删除', '', 'core:role:destroy', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
|
||||
INSERT INTO `sa_system_menu` VALUES (39, 6, '菜单权限', '', 'core:role:menu', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
|
||||
INSERT INTO `sa_system_menu` VALUES (41, 7, '数据列表', '', 'core:post:index', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
|
||||
INSERT INTO `sa_system_menu` VALUES (42, 7, '添加', '', 'core:post:save', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
|
||||
INSERT INTO `sa_system_menu` VALUES (43, 7, '修改', '', 'core:post:update', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
|
||||
INSERT INTO `sa_system_menu` VALUES (44, 7, '读取', '', 'core:post:read', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
|
||||
INSERT INTO `sa_system_menu` VALUES (45, 7, '删除', '', 'core:post:destroy', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
|
||||
INSERT INTO `sa_system_menu` VALUES (46, 7, '导入', '', 'core:post:import', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
|
||||
INSERT INTO `sa_system_menu` VALUES (47, 7, '导出', '', 'core:post:export', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
|
||||
INSERT INTO `sa_system_menu` VALUES (48, 8, '数据列表', '', 'core:menu:index', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
|
||||
INSERT INTO `sa_system_menu` VALUES (49, 8, '读取', '', 'core:menu:read', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
|
||||
INSERT INTO `sa_system_menu` VALUES (50, 8, '添加', '', 'core:menu:save', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
|
||||
@@ -490,25 +482,6 @@ CREATE TABLE `sa_system_oper_log` (
|
||||
-- Records of sa_system_oper_log
|
||||
-- ----------------------------
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for sa_system_post
|
||||
-- ----------------------------
|
||||
DROP TABLE IF EXISTS `sa_system_post`;
|
||||
CREATE TABLE `sa_system_post` (
|
||||
`id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键',
|
||||
`name` varchar(50) NULL DEFAULT NULL COMMENT '岗位名称',
|
||||
`code` varchar(100) NULL DEFAULT NULL COMMENT '岗位代码',
|
||||
`sort` smallint(5) UNSIGNED NULL DEFAULT 0 COMMENT '排序',
|
||||
`status` smallint(6) NULL DEFAULT 1 COMMENT '状态 (1正常 2停用)',
|
||||
`remark` varchar(255) NULL DEFAULT NULL COMMENT '备注',
|
||||
`created_by` int(11) NULL DEFAULT NULL COMMENT '创建者',
|
||||
`updated_by` int(11) NULL DEFAULT NULL COMMENT '更新者',
|
||||
`create_time` datetime(0) NULL DEFAULT NULL COMMENT '创建时间',
|
||||
`update_time` datetime(0) NULL DEFAULT NULL COMMENT '修改时间',
|
||||
`delete_time` datetime(0) NULL DEFAULT NULL COMMENT '删除时间',
|
||||
PRIMARY KEY (`id`) USING BTREE
|
||||
) ENGINE = InnoDB AUTO_INCREMENT = 87 COMMENT = '岗位信息表' ROW_FORMAT = Dynamic;
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for sa_system_role
|
||||
-- ----------------------------
|
||||
@@ -606,23 +579,6 @@ CREATE TABLE `sa_system_user` (
|
||||
-- ----------------------------
|
||||
INSERT INTO `sa_system_user` VALUES (1, 'admin', '$2y$10$wnixh48uDnaW/6D9EygDd.OHJK0vQY/4nHaTjMKBCVDBP2NiTatqS', '祭道之上', '2', 'https://image.saithink.top/saiadmin/avatar.jpg', 'saiadmin@admin.com', '15888888888', 'SaiAdmin是兼具设计美学与高效开发的后台系统!', 'statistics', 1, 1, 1, NULL, NULL, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for sa_system_user_post
|
||||
-- ----------------------------
|
||||
DROP TABLE IF EXISTS `sa_system_user_post`;
|
||||
CREATE TABLE `sa_system_user_post` (
|
||||
`id` bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键',
|
||||
`user_id` bigint(20) UNSIGNED NOT NULL COMMENT '用户主键',
|
||||
`post_id` bigint(20) UNSIGNED NOT NULL COMMENT '岗位主键',
|
||||
PRIMARY KEY (`id`) USING BTREE,
|
||||
INDEX `idx_user_id`(`user_id`) USING BTREE,
|
||||
INDEX `idx_post_id`(`post_id`) USING BTREE
|
||||
) ENGINE = InnoDB AUTO_INCREMENT = 1 COMMENT = '用户与岗位关联表' ROW_FORMAT = Dynamic;
|
||||
|
||||
-- ----------------------------
|
||||
-- Records of sa_system_user_post
|
||||
-- ----------------------------
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for sa_system_user_role
|
||||
-- ----------------------------
|
||||
|
||||
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 [
|
||||
'ACCOUNT_DISABLED' => 'Account is disabled and cannot log in',
|
||||
'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_FORMAT_INVALID' => 'auth-token format invalid',
|
||||
'AUTH_TOKEN_INVALID' => 'auth-token invalid',
|
||||
@@ -254,6 +257,7 @@ return [
|
||||
'This category has sub-categories, please delete them first' => 'This category has sub-categories, please delete them first',
|
||||
'This department has sub-departments, please delete them first' => 'This department has sub-departments, please delete them first',
|
||||
'This department has users, please delete or transfer them first' => 'This department has users, please delete or transfer them first',
|
||||
'This channel has users, please delete or transfer them first' => 'This channel has users, please delete or transfer them first',
|
||||
'This dict code already exists' => 'This dict code already exists',
|
||||
'This menu has sub-menus, please delete them first' => 'This menu has sub-menus, please delete them first',
|
||||
'Timestamp expired or invalid, please sync time' => 'Timestamp expired or invalid, please sync time',
|
||||
|
||||
@@ -4,6 +4,9 @@ declare(strict_types=1);
|
||||
return [
|
||||
'ACCOUNT_DISABLED' => '账号已被禁用,无法登录',
|
||||
'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_FORMAT_INVALID' => 'auth-token 格式无效',
|
||||
'AUTH_TOKEN_INVALID' => 'auth-token 无效',
|
||||
@@ -254,6 +257,7 @@ return [
|
||||
'This category has sub-categories, please delete them first' => '该部门下存在子分类,请先删除子分类',
|
||||
'This department has sub-departments, please delete them first' => '该部门下存在子部门,请先删除子部门',
|
||||
'This department has users, please delete or transfer them first' => '该部门下存在用户,请先删除或者转移用户',
|
||||
'This channel has users, please delete or transfer them first' => '该渠道下存在用户,请先删除或者转移用户',
|
||||
'This dict code already exists' => '该字典标识已存在',
|
||||
'This menu has sub-menus, please delete them first' => '该菜单下存在子菜单,请先删除子菜单',
|
||||
'Timestamp expired or invalid, please sync time' => '时间戳已过期或无效,请同步时间',
|
||||
|
||||
@@ -11,7 +11,7 @@ declare(strict_types=1);
|
||||
|
||||
$options = getopt('', ['agent_id:', 'secret:', 'time::']);
|
||||
|
||||
$agentId = $options['agent_id'] ?? '5ef059938ba799aaa845e1c2e8a762bd';
|
||||
$agentId = $options['agent_id'] ?? '76dc611d6ebaafc66cc0879c71b5db5c';
|
||||
$secret = $options['secret'] ?? 'xF75oK91TQj13s0UmNIr1NBWMWGfflNO';
|
||||
$time = $options['time'] ?? (string) time();
|
||||
|
||||
|
||||