Compare commits
3 Commits
150d31eac5
...
d4cf708bc1
| Author | SHA1 | Date | |
|---|---|---|---|
| d4cf708bc1 | |||
| eb8123c7b3 | |||
| 683bd5d97a |
272
API对接文档.md
Normal file
272
API对接文档.md
Normal file
@@ -0,0 +1,272 @@
|
|||||||
|
# 大富翁-摇色子 对接文档
|
||||||
|
|
||||||
|
本文覆盖**平台侧对接接口**(`/api/v1/*`,使用 `auth-token` 鉴权)以及(可选)**玩家侧接口**(`/api/*`,使用 `token` 鉴权)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 基本约定
|
||||||
|
|
||||||
|
### 1.1 服务地址(Base URL)
|
||||||
|
|
||||||
|
由部署方提供:
|
||||||
|
|
||||||
|
- 测试环境:`https://dice-api.yuliao666.top`
|
||||||
|
|
||||||
|
下文所有路径均为相对路径,如:`/api/v1/getGameUrl`。
|
||||||
|
|
||||||
|
### 1.2 请求方式与编码
|
||||||
|
|
||||||
|
- **请求方法**:项目路由多数使用 `Route::any`,对接建议统一使用 **POST**(便于 body 传参);个别接口文档中标注了 GET 参数。
|
||||||
|
- **编码**:`UTF-8`
|
||||||
|
- **Content-Type**:建议 `application/x-www-form-urlencoded` 或 `application/json`(以平台实际实现为准)
|
||||||
|
|
||||||
|
### 1.3 统一返回结构
|
||||||
|
|
||||||
|
所有接口统一返回 JSON:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 200,
|
||||||
|
"message": "success",
|
||||||
|
"data": {}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- `code`:业务状态码(见“返回 code 对照表”)
|
||||||
|
- `message`:提示信息(可根据请求头 `lang` 返回中英文)
|
||||||
|
- `data`:成功时返回数据;失败时通常不返回该字段
|
||||||
|
|
||||||
|
### 1.4 多语言(lang)
|
||||||
|
|
||||||
|
服务端会根据请求头 `lang` 翻译 `message`(以及部分接口字段):
|
||||||
|
|
||||||
|
- `lang: zh`(默认)
|
||||||
|
- `lang: en`
|
||||||
|
|
||||||
|
> 注意:平台 v1 接口的 `getGameUrl` 还支持 body 参数 `lang`(`zh/en`),用于拼接游戏地址语言参数。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 鉴权与对接流程(平台侧 /api/v1)
|
||||||
|
|
||||||
|
平台侧接口分两步:
|
||||||
|
|
||||||
|
1. **获取 `auth-token`**
|
||||||
|
2. **携带 `auth-token` 调用 `/api/v1/*` 业务接口**
|
||||||
|
|
||||||
|
### 2.1 获取 auth-token
|
||||||
|
|
||||||
|
- **路径**:`/api/v1/authToken`
|
||||||
|
- **方法**:GET / POST 均可(建议 GET,参数放 query)
|
||||||
|
- **说明**:用于平台调用 v1 业务接口的授权 token(JWT)
|
||||||
|
|
||||||
|
#### 请求参数(query/body)
|
||||||
|
|
||||||
|
| 参数名 | 必填 | 类型 | 说明 |
|
||||||
|
| --- | --- | --- |---------------------------------------------------|
|
||||||
|
| agent_id | 是 | string | 平台/代理标识(后台查看对应管理员agent_id) |
|
||||||
|
| secret | 是 | string | 双方约定的密钥(服务端配置 `xF75oK91TQj13s0UmNIr1NBWMWGfflNO`) |
|
||||||
|
| time | 是 | int/string | Unix 时间戳(秒),服务端会做时间容忍校验 |
|
||||||
|
| signature | 是 | string | 签名:`md5(agent_id + secret + time)` |
|
||||||
|
- **agent_id**:通过后台获取系统管理-用户管理-代理ID
|
||||||
|
- **secret**:xF75oK91TQj13s0UmNIr1NBWMWGfflNO(服务器配置)
|
||||||
|
- **后台地址**:https://dice.yuliao666.top 账号: zhuguan 密码:123456
|
||||||
|
#### 签名规则
|
||||||
|
|
||||||
|
- **签名字符串**:直接拼接 `agent_id.secret.time`(无分隔符)
|
||||||
|
- **算法**:MD5(32 位小写/大写均可,按实现一致即可)
|
||||||
|
|
||||||
|
示例(伪代码):
|
||||||
|
|
||||||
|
```text
|
||||||
|
signature = md5(agent_id + secret + time)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 响应示例
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 200,
|
||||||
|
"message": "success",
|
||||||
|
"data": {
|
||||||
|
"authtoken": "xxxx.yyyy.zzzz"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 失败场景
|
||||||
|
|
||||||
|
- 缺少参数:`code=400`
|
||||||
|
- 密钥错误/签名错误/时间戳无效:`code=403`
|
||||||
|
- 服务端未配置密钥或生成失败:`code=500`
|
||||||
|
|
||||||
|
### 2.2 调用 v1 业务接口(携带 auth-token)
|
||||||
|
|
||||||
|
除 `/api/v1/authToken` 外,其余 `/api/v1/*` 接口需要在请求头携带:
|
||||||
|
|
||||||
|
- `auth-token: <authtoken>`
|
||||||
|
|
||||||
|
当 `auth-token` 过期或失效,返回 `code=402`,需要重新调用 `/api/v1/authToken` 获取新 token。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 平台 v1 业务接口清单(/api/v1)
|
||||||
|
|
||||||
|
### 3.1 获取游戏地址
|
||||||
|
|
||||||
|
- **路径**:`/api/v1/getGameUrl`
|
||||||
|
- **方法**:POST
|
||||||
|
- **请求头**:`auth-token`
|
||||||
|
- **说明**:根据平台用户名创建/登录玩家并生成登录 JWT,返回可直接打开的游戏地址。
|
||||||
|
|
||||||
|
#### 请求参数(body)
|
||||||
|
|
||||||
|
| 参数名 | 必填 | 类型 | 说明 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| username | 是 | string | 玩家唯一账号(平台侧用户名) |
|
||||||
|
| password | 否 | string | 默认 `123456` |
|
||||||
|
| time | 否 | int/string | 默认当前时间戳 |
|
||||||
|
| lang | 否 | string | `zh` / `en`,默认 `zh` |
|
||||||
|
|
||||||
|
#### 响应示例
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 200,
|
||||||
|
"message": "success",
|
||||||
|
"data": {
|
||||||
|
"url": "https://dice-game.yuliao666.top/?token=...&lang=zh"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.2 获取用户信息
|
||||||
|
|
||||||
|
- **路径**:`/api/v1/getPlayerInfo`
|
||||||
|
- **方法**:POST
|
||||||
|
- **请求头**:`auth-token`
|
||||||
|
|
||||||
|
#### 请求参数
|
||||||
|
|
||||||
|
| 参数名 | 必填 | 类型 | 说明 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| username | 是 | string | 玩家账号 |
|
||||||
|
|
||||||
|
#### 响应说明
|
||||||
|
|
||||||
|
返回玩家基础信息(已隐藏敏感字段,如 `password` 等)。
|
||||||
|
|
||||||
|
### 3.3 获取游戏记录
|
||||||
|
|
||||||
|
- **路径**:`/api/v1/getPlayerGameRecord`
|
||||||
|
- **方法**:POST
|
||||||
|
- **请求头**:`auth-token`
|
||||||
|
|
||||||
|
#### 请求参数
|
||||||
|
|
||||||
|
| 参数名 | 必填 | 类型 | 说明 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| username | 否 | string | 不传则查询全量(按时间/分页) |
|
||||||
|
| start_create_time | 否 | string | 开始时间(与数据库存储格式一致) |
|
||||||
|
| end_create_time | 否 | string | 结束时间(与数据库存储格式一致) |
|
||||||
|
| page | 否 | int | 默认 1 |
|
||||||
|
| limit | 否 | int | 默认 20,最大 100 |
|
||||||
|
|
||||||
|
#### 响应说明
|
||||||
|
|
||||||
|
返回游玩记录列表,并附带 `dice_player`(包含 `id/username/phone`)。
|
||||||
|
|
||||||
|
### 3.4 获取钱包流水
|
||||||
|
|
||||||
|
- **路径**:`/api/v1/getPlayerWalletRecord`
|
||||||
|
- **方法**:POST
|
||||||
|
- **请求头**:`auth-token`
|
||||||
|
|
||||||
|
参数与分页规则同 3.3,返回钱包流水列表(附带 `dice_player`)。
|
||||||
|
|
||||||
|
### 3.5 获取中奖券获取记录
|
||||||
|
|
||||||
|
- **路径**:`/api/v1/getPlayerTicketRecord`
|
||||||
|
- **方法**:POST
|
||||||
|
- **请求头**:`auth-token`
|
||||||
|
|
||||||
|
参数与分页规则同 3.3,返回中奖券记录列表(附带 `dice_player`)。
|
||||||
|
|
||||||
|
### 3.6 平台钱包转入/转出
|
||||||
|
|
||||||
|
- **路径**:`/api/v1/setPlayerWallet`
|
||||||
|
- **方法**:POST
|
||||||
|
- **请求头**:`auth-token`
|
||||||
|
- **说明**:平台为玩家加币/扣币,生成钱包流水。
|
||||||
|
|
||||||
|
#### 请求参数
|
||||||
|
|
||||||
|
| 参数名 | 必填 | 类型 | 说明 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| username | 是 | string | 玩家账号 |
|
||||||
|
| coin | 是 | number | 转入:`>0`;转出:`<0`;不允许为 0 |
|
||||||
|
|
||||||
|
#### 业务规则
|
||||||
|
|
||||||
|
- 当 `coin < 0` 且余额不足时:`code=422`,`message=余额不足,无法转出`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 玩家侧接口(可选 /api)
|
||||||
|
|
||||||
|
若甲方平台需要在**服务端**直接调用“购买抽奖券/开局”等接口,可按本节对接(此类接口需要 `token`,与 `auth-token` 不同)。
|
||||||
|
|
||||||
|
### 4.1 获取 token(玩家登录)
|
||||||
|
|
||||||
|
- **路径**:`/api/user/Login`
|
||||||
|
- **方法**:POST
|
||||||
|
- **说明**:创建/登录玩家并返回登录 token 以及游戏地址。
|
||||||
|
|
||||||
|
请求参数:`username`、`password`(必填),`lang/coin/time`(可选)。
|
||||||
|
|
||||||
|
响应 `data.token` 可用于后续 `/api/*` 请求头 `token` 或 `Authorization: Bearer <token>`。
|
||||||
|
|
||||||
|
### 4.2 需要 token 的接口
|
||||||
|
|
||||||
|
请求头携带(任选一种):
|
||||||
|
|
||||||
|
- `token: <token>`
|
||||||
|
- `Authorization: Bearer <token>`
|
||||||
|
|
||||||
|
接口列表:
|
||||||
|
|
||||||
|
- `/api/user/logout`
|
||||||
|
- `/api/user/info`
|
||||||
|
- `/api/user/balance`
|
||||||
|
- `/api/user/walletRecord`
|
||||||
|
- `/api/user/playGameRecord`
|
||||||
|
- `/api/game/config`
|
||||||
|
- `/api/game/buyLotteryTickets`
|
||||||
|
- `/api/game/lotteryPool`
|
||||||
|
- `/api/game/playStart`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 返回 code 对照表
|
||||||
|
|
||||||
|
项目定义的统一状态码如下(与 HTTP 语义对齐):
|
||||||
|
|
||||||
|
| code | 含义 | 常见场景 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 200 | 成功 | 请求成功 |
|
||||||
|
| 400 | 请求参数错误 | 缺参、参数格式不合法、范围错误 |
|
||||||
|
| 401 | 未授权 | 未携带 `auth-token` 或 `token` |
|
||||||
|
| 402 | token 无效或已过期 | `auth-token/token` 过期、签名错误、被挤下线等 |
|
||||||
|
| 403 | 鉴权失败 | `secret` 错误、签名验证失败、时间戳无效等 |
|
||||||
|
| 404 | 资源不存在 | 用户不存在等 |
|
||||||
|
| 422 | 业务逻辑错误 | 余额不足、业务校验失败等 |
|
||||||
|
| 500 | 服务器内部错误 | 服务端异常或配置缺失 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 联调建议
|
||||||
|
|
||||||
|
- **时间戳校验**:`/api/v1/authToken` 会校验 `time` 与服务器时间差(默认容忍 300 秒),请确保平台服务器时间同步。
|
||||||
|
- **token 失效处理**:当 `code=402` 时,按业务类型重新获取 `auth-token` 或重新登录获取 `token`。
|
||||||
|
- **语言**:如需英文提示,请在请求头带 `lang: en`。
|
||||||
|
|
||||||
@@ -16,11 +16,19 @@
|
|||||||
|
|
||||||
import axios, { AxiosRequestConfig, AxiosResponse, InternalAxiosRequestConfig } from 'axios'
|
import axios, { AxiosRequestConfig, AxiosResponse, InternalAxiosRequestConfig } from 'axios'
|
||||||
import { useUserStore } from '@/store/modules/user'
|
import { useUserStore } from '@/store/modules/user'
|
||||||
|
import i18n from '@/locales'
|
||||||
import { ApiStatus } from './status'
|
import { ApiStatus } from './status'
|
||||||
import { HttpError, handleError, showError, showSuccess } from './error'
|
import { HttpError, handleError, showError, showSuccess } from './error'
|
||||||
import { $t } from '@/locales'
|
import { $t } from '@/locales'
|
||||||
import { BaseResponse } from '@/types'
|
import { BaseResponse } from '@/types'
|
||||||
|
|
||||||
|
/** 当前语言(zh/en),供请求头 lang 使用,无 header 时后端按后台语言回包 */
|
||||||
|
function getRequestLang(): string {
|
||||||
|
const locale = i18n.global.locale as string | { value: string }
|
||||||
|
const lang = typeof locale === 'string' ? locale : (locale && 'value' in locale ? locale.value : 'zh')
|
||||||
|
return lang === 'en' ? 'en' : 'zh'
|
||||||
|
}
|
||||||
|
|
||||||
/** 请求配置常量(超时时间 30s) */
|
/** 请求配置常量(超时时间 30s) */
|
||||||
const REQUEST_TIMEOUT = 30000
|
const REQUEST_TIMEOUT = 30000
|
||||||
const LOGOUT_DELAY = 500
|
const LOGOUT_DELAY = 500
|
||||||
@@ -66,6 +74,7 @@ axiosInstance.interceptors.request.use(
|
|||||||
(request: InternalAxiosRequestConfig) => {
|
(request: InternalAxiosRequestConfig) => {
|
||||||
const { accessToken } = useUserStore()
|
const { accessToken } = useUserStore()
|
||||||
if (accessToken) request.headers.set('Authorization', `Bearer ` + accessToken)
|
if (accessToken) request.headers.set('Authorization', `Bearer ` + accessToken)
|
||||||
|
request.headers.set('lang', getRequestLang())
|
||||||
|
|
||||||
if (request.data && !(request.data instanceof FormData) && !request.headers['Content-Type']) {
|
if (request.data && !(request.data instanceof FormData) && !request.headers['Content-Type']) {
|
||||||
request.headers.set('Content-Type', 'application/json')
|
request.headers.set('Content-Type', 'application/json')
|
||||||
|
|||||||
@@ -36,7 +36,53 @@ return [
|
|||||||
// PlayStartLogic / GameLogic
|
// PlayStartLogic / GameLogic
|
||||||
'抽奖券不足' => 'Insufficient lottery tickets',
|
'抽奖券不足' => 'Insufficient lottery tickets',
|
||||||
'奖池配置不存在' => 'Lottery config not found',
|
'奖池配置不存在' => 'Lottery config not found',
|
||||||
|
'配置ID %s 不存在或档位为空' => 'Config ID %s not found or tier is empty',
|
||||||
'该方向下暂无可用路径配置' => 'No path config available for this direction',
|
'该方向下暂无可用路径配置' => 'No path config available for this direction',
|
||||||
|
// Dice / pool config
|
||||||
|
'奖池配置不存在(需 type=0)' => 'Lottery pool config not found (type=0 required)',
|
||||||
|
'暂无可用奖励配置' => 'No available reward config',
|
||||||
|
'未找到 type=0 的奖池配置,请先创建' => 'No type=0 pool config found, please create one first',
|
||||||
|
// Dice / wallet & tickets
|
||||||
|
'参数错误:需要有效的 player_id 和 type(3=加点,4=扣点)' => 'Invalid params: player_id and type are required (3=add, 4=deduct)',
|
||||||
|
'平台币变动必须大于 0' => 'Coin change must be greater than 0',
|
||||||
|
'玩家不存在' => 'Player not found',
|
||||||
|
'扣点数量不能大于当前余额' => 'Deduct amount cannot exceed current balance',
|
||||||
|
// Dice / reward config record
|
||||||
|
'测试记录不存在' => 'Test record not found',
|
||||||
|
'付费奖池配置不存在' => 'Paid pool config not found',
|
||||||
|
'免费奖池配置不存在' => 'Free pool config not found',
|
||||||
|
'各抽奖次数仅支持 0、100、500、1000、5000' => 'Counts only support 0, 100, 500, 1000, 5000',
|
||||||
|
'付费或免费至少一种方向次数之和大于 0' => 'Sum of paid/free direction counts must be greater than 0',
|
||||||
|
'付费未选择奖池配置时,请填写付费自定义档位概率(T1~T5)' => 'When paid pool is not selected, please fill paid custom tier probabilities (T1–T5)',
|
||||||
|
'付费档位概率每档只能 0-100%' => 'Paid tier probability must be between 0 and 100%',
|
||||||
|
'付费档位概率 T1~T5 之和不能超过 100%' => 'Paid tier probabilities (T1–T5) sum cannot exceed 100%',
|
||||||
|
'免费未选择奖池配置时,请填写免费自定义档位概率(T1~T5)' => 'When free pool is not selected, please fill free custom tier probabilities (T1–T5)',
|
||||||
|
'免费档位概率每档只能 0-100%' => 'Free tier probability must be between 0 and 100%',
|
||||||
|
'免费档位概率 T1~T5 之和不能超过 100%' => 'Free tier probabilities (T1–T5) sum cannot exceed 100%',
|
||||||
|
// Dice / reward
|
||||||
|
'存在无效的配置ID' => 'Invalid config ID exists',
|
||||||
|
'存在无效的 DiceReward id' => 'Invalid DiceReward id exists',
|
||||||
|
'奖励配置为空,请先维护 dice_reward_config' => 'Reward config is empty, please maintain dice_reward_config first',
|
||||||
|
// Dice / reward_config
|
||||||
|
'测试次数仅支持 100、500、1000、5000、10000' => 'Test count only supports 100, 500, 1000, 5000, 10000',
|
||||||
|
// SaiAdmin permissions & auth
|
||||||
|
'没有权限操作该部门数据' => 'No permission to operate department data',
|
||||||
|
'没有权限操作该角色数据' => 'No permission to operate role data',
|
||||||
|
'没有权限操作该数据' => 'No permission to operate this data',
|
||||||
|
'禁止批量删除操作' => 'Batch delete is not allowed',
|
||||||
|
'超级管理员禁止删除' => 'Super admin cannot be deleted',
|
||||||
|
'原密码错误' => 'Old password is incorrect',
|
||||||
|
'上级部门和当前部门不能相同' => 'Parent department cannot be the same as current department',
|
||||||
|
'不能将上级部门设置为当前部门的子部门' => 'Cannot set parent department to a child of current department',
|
||||||
|
'该部门下存在子部门,请先删除子部门' => 'This department has sub-departments, please delete them first',
|
||||||
|
'该部门下存在用户,请先删除或者转移用户' => 'This department has users, please delete or transfer them first',
|
||||||
|
'您的登录凭证错误或者已过期,请重新登录' => 'Your login credential is invalid or expired, please login again',
|
||||||
|
'登录凭证校验失败' => 'Login credential verification failed',
|
||||||
|
// Saipackage install
|
||||||
|
'插件的基础配置信息错误' => 'Plugin base config is invalid',
|
||||||
|
'插件已经存在' => 'Plugin already exists',
|
||||||
|
'该插件的安装目录已经被占用' => 'Plugin install directory is already occupied',
|
||||||
|
'文件不存在' => 'File not found',
|
||||||
// UserLogic
|
// UserLogic
|
||||||
'手机号格式错误,仅支持 +60 开头的马来西亚号码(如 +60123456789)' => 'Invalid phone format, only +60 Malaysia numbers supported (e.g. +60123456789)',
|
'手机号格式错误,仅支持 +60 开头的马来西亚号码(如 +60123456789)' => 'Invalid phone format, only +60 Malaysia numbers supported (e.g. +60123456789)',
|
||||||
// TokenMiddleware / Auth (api/user/*, api/game/*)
|
// TokenMiddleware / Auth (api/user/*, api/game/*)
|
||||||
@@ -50,4 +96,57 @@ return [
|
|||||||
'请注册' => 'Please register',
|
'请注册' => 'Please register',
|
||||||
'请重新登录' => 'Please login again',
|
'请重新登录' => 'Please login again',
|
||||||
'请重新登录(当前账号已在其他处登录)' => 'Please login again (account logged in elsewhere)',
|
'请重新登录(当前账号已在其他处登录)' => 'Please login again (account logged in elsewhere)',
|
||||||
|
// DiceRewardLogic 动态文案(占位符)
|
||||||
|
'奖励配置需覆盖 26 个格位(id 0-25 或 1-26),当前仅 %s 条,无法完整生成 5-30 共26个点数、顺时针与逆时针的奖励对照' => 'Reward config must cover 26 cells (id 0-25 or 1-26), currently only %s, cannot generate full 5-30 points and clockwise/counterclockwise mapping',
|
||||||
|
// SystemUserLogic / BaseController 等(validate 等动态 message 无 key,保留原文)
|
||||||
|
// CheckLogin
|
||||||
|
// BaseLogic / Crontab / Menu / Post / Role / Dict / Config / Category / Attachment / Database
|
||||||
|
'数据不存在' => 'Data not found',
|
||||||
|
'不能设置父级为自身' => 'Cannot set parent to self',
|
||||||
|
'该菜单下存在子菜单,请先删除子菜单' => 'This menu has sub-menus, please delete them first',
|
||||||
|
'导入文件错误,请上传正确的文件格式xlsx' => 'Import file error, please upload correct xlsx file',
|
||||||
|
'不能操作比当前账户职级高的角色' => 'Cannot operate roles with higher level than current account',
|
||||||
|
'该字典标识已存在' => 'This dict code already exists',
|
||||||
|
'修改数据异常,请检查' => 'Update data error, please check',
|
||||||
|
'删除数据异常,请检查' => 'Delete data error, please check',
|
||||||
|
'字典类型不存在' => 'Dict type not found',
|
||||||
|
'配置数据未找到' => 'Config data not found',
|
||||||
|
'系统默认分组,无法删除' => 'System default group cannot be deleted',
|
||||||
|
'配置组未找到' => 'Config group not found',
|
||||||
|
'上级分类和当前分类不能相同' => 'Parent category cannot be the same as current',
|
||||||
|
'不能将上级分类设置为当前分类的子分类' => 'Cannot set parent category as child of current',
|
||||||
|
'该部门下存在子分类,请先删除子分类' => 'This category has sub-categories, please delete them first',
|
||||||
|
'目标分类不存在' => 'Target category not found',
|
||||||
|
'获取文件资源失败' => 'Failed to get file resource',
|
||||||
|
'创建图片资源失败' => 'Failed to create image resource',
|
||||||
|
'文件格式错误' => 'Invalid file format',
|
||||||
|
'文件保存失败' => 'Failed to save file',
|
||||||
|
'当前表不支持回收站功能' => 'Current table does not support recycle bin',
|
||||||
|
'模板不存在' => 'Template not found',
|
||||||
|
'任务类型异常' => 'Invalid task type',
|
||||||
|
'数据库配置读取失败' => 'Failed to read database config',
|
||||||
|
'应用类型必须为plugin或者app' => 'App type must be plugin or app',
|
||||||
|
'请先设置应用名称' => 'Please set app name first',
|
||||||
|
'请选择要生成的表' => 'Please select tables to generate',
|
||||||
|
'非调试模式下,不允许生成文件' => 'File generation not allowed in non-debug mode',
|
||||||
|
'登录凭获取失败,请检查' => 'Failed to get login credential, please check',
|
||||||
|
'文件大小超过限制' => 'File size exceeds limit',
|
||||||
|
'不支持该格式的文件上传' => 'File format not supported for upload',
|
||||||
|
'该上传模式不存在' => 'Upload mode not found',
|
||||||
|
'切片上传服务必须在 HTTP 请求环境下调用' => 'Chunk upload must be called in HTTP request context',
|
||||||
|
'切片文件查找失败,请重新上传' => 'Chunk file not found, please upload again',
|
||||||
|
'未设置邮件配置' => 'Mail config not set',
|
||||||
|
'请执行 composer require phpmailer/phpmailer 并重启' => 'Please run composer require phpmailer/phpmailer and restart',
|
||||||
|
'仅超级管理员能够操作' => 'Only super admin can perform this action',
|
||||||
|
'等待依赖安装' => 'Waiting for dependencies to be installed',
|
||||||
|
'插件目录不存在' => 'Plugin directory not found',
|
||||||
|
'该插件的基础配置信息不完善' => 'Plugin base config is incomplete',
|
||||||
|
'参数错误' => 'Invalid parameters',
|
||||||
|
'不能设置父级为自身' => 'Cannot set parent to self',
|
||||||
|
'该分类下存在子分类,请先删除子分类' => 'This category has sub-categories, please delete them first',
|
||||||
|
'无法打开文件,或者文件创建失败' => 'Cannot open file or create file failed',
|
||||||
|
'系统生成文件错误' => 'System file generation error',
|
||||||
|
'模板目录不存在!' => 'Template directory not found',
|
||||||
|
'文件类型异常,无法生成指定文件!' => 'Invalid file type, cannot generate file',
|
||||||
|
'前端目录查找失败,必须与后端目录为同级目录!' => 'Frontend directory not found, must be same level as backend',
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
|||||||
namespace app\api\logic;
|
namespace app\api\logic;
|
||||||
|
|
||||||
use app\api\cache\UserCache;
|
use app\api\cache\UserCache;
|
||||||
|
use app\api\util\ApiLang;
|
||||||
use app\api\service\LotteryService;
|
use app\api\service\LotteryService;
|
||||||
use app\dice\model\lottery_pool_config\DiceLotteryPoolConfig;
|
use app\dice\model\lottery_pool_config\DiceLotteryPoolConfig;
|
||||||
use app\dice\model\play_record\DicePlayRecord;
|
use app\dice\model\play_record\DicePlayRecord;
|
||||||
@@ -59,7 +60,7 @@ class PlayStartLogic
|
|||||||
$minCoin = abs($minEv + self::MIN_COIN_EXTRA);
|
$minCoin = abs($minEv + self::MIN_COIN_EXTRA);
|
||||||
$coin = (float) $player->coin;
|
$coin = (float) $player->coin;
|
||||||
if ($coin < $minCoin) {
|
if ($coin < $minCoin) {
|
||||||
throw new ApiException('当前玩家余额'.$coin.'小于'.$minCoin.'无法继续游戏');
|
throw new ApiException(ApiLang::translateParams('当前玩家余额%s小于%s无法继续游戏', [$coin, $minCoin]));
|
||||||
}
|
}
|
||||||
|
|
||||||
$paid = (int) ($player->paid_ticket_count ?? 0);
|
$paid = (int) ($player->paid_ticket_count ?? 0);
|
||||||
|
|||||||
@@ -6,7 +6,9 @@ namespace app\api\util;
|
|||||||
use support\Request;
|
use support\Request;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* API 多语言:根据请求头 lang(en=英文,zh=中文)翻译返回文案
|
* API 多语言(兼容 Webman 多语言配置)
|
||||||
|
* 根据请求头 lang(zh=中文,en=英文)返回对应文案;
|
||||||
|
* 无 lang 请求头时使用 config('translation.locale') 推断(zh_CN/zh→中文,en→英文)
|
||||||
*/
|
*/
|
||||||
class ApiLang
|
class ApiLang
|
||||||
{
|
{
|
||||||
@@ -14,35 +16,34 @@ class ApiLang
|
|||||||
private const LANG_EN = 'en';
|
private const LANG_EN = 'en';
|
||||||
private const LANG_ZH = 'zh';
|
private const LANG_ZH = 'zh';
|
||||||
|
|
||||||
/** @var array<string, string>|null */
|
/** @var array<string, array<string, string>> locale => [ 中文 => 译文 ] */
|
||||||
private static ?array $enMap = null;
|
private static array $messages = [];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 从请求中获取语言:lang 请求头 en=英文,zh=中文,默认 zh
|
* 从请求中获取语言:优先读 header lang,否则按 Webman config('translation.locale') 推断
|
||||||
*/
|
*/
|
||||||
public static function getLang(?Request $request = null): string
|
public static function getLang(?Request $request = null): string
|
||||||
{
|
{
|
||||||
$request = $request ?? (function_exists('request') ? request() : null);
|
$request = $request ?? (function_exists('request') ? request() : null);
|
||||||
if ($request === null) {
|
if ($request !== null) {
|
||||||
return self::LANG_ZH;
|
$lang = $request->header(self::LANG_HEADER);
|
||||||
}
|
if ($lang !== null && $lang !== '') {
|
||||||
$lang = $request->header(self::LANG_HEADER);
|
$lang = strtolower(trim((string) $lang));
|
||||||
if ($lang !== null && $lang !== '') {
|
if ($lang === self::LANG_EN) {
|
||||||
$lang = strtolower(trim((string) $lang));
|
return self::LANG_EN;
|
||||||
if ($lang === self::LANG_EN) {
|
}
|
||||||
return self::LANG_EN;
|
if ($lang === self::LANG_ZH || $lang === 'chs') {
|
||||||
}
|
return self::LANG_ZH;
|
||||||
if ($lang === self::LANG_ZH || $lang === 'chs') {
|
}
|
||||||
return self::LANG_ZH;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return self::LANG_ZH;
|
$locale = (string) (function_exists('config') ? config('translation.locale', 'zh_CN') : 'zh_CN');
|
||||||
|
return stripos($locale, 'en') !== false ? self::LANG_EN : self::LANG_ZH;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 翻译文案:当前请求语言为 en 时返回英文,否则返回原文(中文)
|
* 翻译文案:lang=zh 返回原文(中文),lang=en 返回英文映射
|
||||||
* @param string $message 中文或原文
|
* 语言文件优先从 Webman resource/translations/api/{locale}.php 加载,否则从 app/api/lang 加载
|
||||||
* @param Request|null $request 当前请求,不传则自动取 request()
|
|
||||||
*/
|
*/
|
||||||
public static function translate(string $message, ?Request $request = null): string
|
public static function translate(string $message, ?Request $request = null): string
|
||||||
{
|
{
|
||||||
@@ -50,11 +51,30 @@ class ApiLang
|
|||||||
if ($lang !== self::LANG_EN) {
|
if ($lang !== self::LANG_EN) {
|
||||||
return $message;
|
return $message;
|
||||||
}
|
}
|
||||||
if (self::$enMap === null) {
|
$map = self::loadMessages(self::LANG_EN);
|
||||||
$path = dirname(__DIR__) . '/lang/en.php';
|
return $map[$message] ?? $message;
|
||||||
self::$enMap = is_file($path) ? (require $path) : [];
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加载某语言的 API 文案(key=中文,value=译文)
|
||||||
|
*/
|
||||||
|
private static function loadMessages(string $locale): array
|
||||||
|
{
|
||||||
|
if (isset(self::$messages[$locale])) {
|
||||||
|
return self::$messages[$locale];
|
||||||
}
|
}
|
||||||
return self::$enMap[$message] ?? $message;
|
$path = null;
|
||||||
|
if (function_exists('config')) {
|
||||||
|
$base = rtrim((string) config('translation.path', ''), DIRECTORY_SEPARATOR);
|
||||||
|
if ($base !== '') {
|
||||||
|
$path = $base . DIRECTORY_SEPARATOR . 'api' . DIRECTORY_SEPARATOR . $locale . '.php';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (($path === null || !is_file($path)) && $locale === self::LANG_EN) {
|
||||||
|
$path = dirname(__DIR__) . DIRECTORY_SEPARATOR . 'lang' . DIRECTORY_SEPARATOR . 'en.php';
|
||||||
|
}
|
||||||
|
self::$messages[$locale] = ($path !== null && is_file($path)) ? (require $path) : [];
|
||||||
|
return self::$messages[$locale];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ class DiceRewardLogic
|
|||||||
|
|
||||||
$tier = DiceRewardConfig::where('id', $id)->value('tier');
|
$tier = DiceRewardConfig::where('id', $id)->value('tier');
|
||||||
if ($tier === null || $tier === '') {
|
if ($tier === null || $tier === '') {
|
||||||
throw new ApiException('配置ID ' . $id . ' 不存在或档位为空');
|
throw new ApiException(\app\api\util\ApiLang::translateParams('配置ID %s 不存在或档位为空', [$id]));
|
||||||
}
|
}
|
||||||
$tier = (string) $tier;
|
$tier = (string) $tier;
|
||||||
|
|
||||||
@@ -318,7 +318,10 @@ class DiceRewardLogic
|
|||||||
$configCount = count($list);
|
$configCount = count($list);
|
||||||
if ($configCount < self::BOARD_SIZE) {
|
if ($configCount < self::BOARD_SIZE) {
|
||||||
throw new ApiException(
|
throw new ApiException(
|
||||||
'奖励配置需覆盖 26 个格位(id 0-25 或 1-26),当前仅 ' . $configCount . ' 条,无法完整生成 5-30 共26个点数、顺时针与逆时针的奖励对照'
|
\app\api\util\ApiLang::translateParams(
|
||||||
|
'奖励配置需覆盖 26 个格位(id 0-25 或 1-26),当前仅 %s 条,无法完整生成 5-30 共26个点数、顺时针与逆时针的奖励对照',
|
||||||
|
[$configCount]
|
||||||
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,10 +18,7 @@ class ApiException extends BusinessException
|
|||||||
public function render(Request $request): ?Response
|
public function render(Request $request): ?Response
|
||||||
{
|
{
|
||||||
$message = $this->getMessage();
|
$message = $this->getMessage();
|
||||||
$path = $request->path();
|
$message = \app\api\util\ApiLang::translate($message, $request);
|
||||||
if (str_contains($path, 'api/')) {
|
|
||||||
$message = \app\api\util\ApiLang::translate($message, $request);
|
|
||||||
}
|
|
||||||
return json(['code' => $this->getCode() ?: 500, 'message' => $message]);
|
return json(['code' => $this->getCode() ?: 500, 'message' => $message]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
136
server/resource/translations/api/en.php
Normal file
136
server/resource/translations/api/en.php
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* API 英文文案(Webman 多语言路径:config translation.path / api / en.php)
|
||||||
|
* 请求头 lang=en 时使用,key 为中文原文,value 为英文
|
||||||
|
*/
|
||||||
|
return [
|
||||||
|
'success' => 'Success',
|
||||||
|
'fail' => 'Fail',
|
||||||
|
'username、password 不能为空' => 'username and password are required',
|
||||||
|
'请携带 token' => 'Please provide token',
|
||||||
|
'token 无效' => 'Invalid or expired token',
|
||||||
|
'已退出登录' => 'Logged out successfully',
|
||||||
|
'用户不存在' => 'User not found',
|
||||||
|
'username 不能为空' => 'username is required',
|
||||||
|
'密码错误' => 'Wrong password',
|
||||||
|
'账号已被禁用,无法登录' => 'Account is disabled and cannot log in',
|
||||||
|
'购买抽奖券错误' => 'Invalid lottery ticket purchase',
|
||||||
|
'平台币不足' => 'Insufficient balance',
|
||||||
|
'direction 必须为 0 或 1' => 'direction must be 0 or 1',
|
||||||
|
'当前玩家余额%s小于%s无法继续游戏' => 'Balance %s is less than %s, cannot continue',
|
||||||
|
'服务超时,' => 'Service timeout: ',
|
||||||
|
'没有原因' => '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',
|
||||||
|
'密钥错误' => 'Invalid secret',
|
||||||
|
'时间戳已过期或无效,请同步时间' => 'Timestamp expired or invalid, please sync time',
|
||||||
|
'签名验证失败' => 'Signature verification failed',
|
||||||
|
'生成 token 失败' => 'Failed to generate token',
|
||||||
|
'coin 不能为空' => 'coin is required',
|
||||||
|
'coin 不能为 0' => 'coin cannot be 0',
|
||||||
|
'余额不足,无法转出' => 'Insufficient balance to transfer',
|
||||||
|
'操作失败:' => 'Operation failed: ',
|
||||||
|
'服务超时,没有原因' => 'Service timeout: Unknown reason',
|
||||||
|
'抽奖券不足' => 'Insufficient lottery tickets',
|
||||||
|
'奖池配置不存在' => 'Lottery config not found',
|
||||||
|
'配置ID %s 不存在或档位为空' => 'Config ID %s not found or tier is empty',
|
||||||
|
'该方向下暂无可用路径配置' => 'No path config available for this direction',
|
||||||
|
'奖池配置不存在(需 type=0)' => 'Lottery pool config not found (type=0 required)',
|
||||||
|
'暂无可用奖励配置' => 'No available reward config',
|
||||||
|
'未找到 type=0 的奖池配置,请先创建' => 'No type=0 pool config found, please create one first',
|
||||||
|
'参数错误:需要有效的 player_id 和 type(3=加点,4=扣点)' => 'Invalid params: player_id and type are required (3=add, 4=deduct)',
|
||||||
|
'平台币变动必须大于 0' => 'Coin change must be greater than 0',
|
||||||
|
'玩家不存在' => 'Player not found',
|
||||||
|
'扣点数量不能大于当前余额' => 'Deduct amount cannot exceed current balance',
|
||||||
|
'测试记录不存在' => 'Test record not found',
|
||||||
|
'付费奖池配置不存在' => 'Paid pool config not found',
|
||||||
|
'免费奖池配置不存在' => 'Free pool config not found',
|
||||||
|
'各抽奖次数仅支持 0、100、500、1000、5000' => 'Counts only support 0, 100, 500, 1000, 5000',
|
||||||
|
'付费或免费至少一种方向次数之和大于 0' => 'Sum of paid/free direction counts must be greater than 0',
|
||||||
|
'付费未选择奖池配置时,请填写付费自定义档位概率(T1~T5)' => 'When paid pool is not selected, please fill paid custom tier probabilities (T1–T5)',
|
||||||
|
'付费档位概率每档只能 0-100%' => 'Paid tier probability must be between 0 and 100%',
|
||||||
|
'付费档位概率 T1~T5 之和不能超过 100%' => 'Paid tier probabilities (T1–T5) sum cannot exceed 100%',
|
||||||
|
'免费未选择奖池配置时,请填写免费自定义档位概率(T1~T5)' => 'When free pool is not selected, please fill free custom tier probabilities (T1–T5)',
|
||||||
|
'免费档位概率每档只能 0-100%' => 'Free tier probability must be between 0 and 100%',
|
||||||
|
'免费档位概率 T1~T5 之和不能超过 100%' => 'Free tier probabilities (T1–T5) sum cannot exceed 100%',
|
||||||
|
'存在无效的配置ID' => 'Invalid config ID exists',
|
||||||
|
'存在无效的 DiceReward id' => 'Invalid DiceReward id exists',
|
||||||
|
'奖励配置为空,请先维护 dice_reward_config' => 'Reward config is empty, please maintain dice_reward_config first',
|
||||||
|
'奖励配置需覆盖 26 个格位(id 0-25 或 1-26),当前仅 %s 条,无法完整生成 5-30 共26个点数、顺时针与逆时针的奖励对照' => 'Reward config must cover 26 cells (id 0-25 or 1-26), currently only %s, cannot generate full 5-30 points and clockwise/counterclockwise mapping',
|
||||||
|
'测试次数仅支持 100、500、1000、5000、10000' => 'Test count only supports 100, 500, 1000, 5000, 10000',
|
||||||
|
'没有权限操作该部门数据' => 'No permission to operate department data',
|
||||||
|
'没有权限操作该角色数据' => 'No permission to operate role data',
|
||||||
|
'没有权限操作该数据' => 'No permission to operate this data',
|
||||||
|
'禁止批量删除操作' => 'Batch delete is not allowed',
|
||||||
|
'超级管理员禁止删除' => 'Super admin cannot be deleted',
|
||||||
|
'原密码错误' => 'Old password is incorrect',
|
||||||
|
'上级部门和当前部门不能相同' => 'Parent department cannot be the same as current department',
|
||||||
|
'不能将上级部门设置为当前部门的子部门' => 'Cannot set parent department to a child of current department',
|
||||||
|
'该部门下存在子部门,请先删除子部门' => 'This department has sub-departments, please delete them first',
|
||||||
|
'该部门下存在用户,请先删除或者转移用户' => 'This department has users, please delete or transfer them first',
|
||||||
|
'您的登录凭证错误或者已过期,请重新登录' => 'Your login credential is invalid or expired, please login again',
|
||||||
|
'登录凭证校验失败' => 'Login credential verification failed',
|
||||||
|
'插件的基础配置信息错误' => 'Plugin base config is invalid',
|
||||||
|
'插件已经存在' => 'Plugin already exists',
|
||||||
|
'该插件的安装目录已经被占用' => 'Plugin install directory is already occupied',
|
||||||
|
'文件不存在' => 'File not found',
|
||||||
|
'手机号格式错误,仅支持 +60 开头的马来西亚号码(如 +60123456789)' => 'Invalid phone format, only +60 Malaysia numbers supported (e.g. +60123456789)',
|
||||||
|
'请携带 auth-token' => 'Please provide auth-token',
|
||||||
|
'auth-token 已过期' => 'auth-token expired',
|
||||||
|
'auth-token 无效' => 'auth-token invalid',
|
||||||
|
'auth-token 格式无效' => 'auth-token format invalid',
|
||||||
|
'auth-token 无效或已失效' => 'auth-token invalid or expired',
|
||||||
|
'token 已过期,请重新登录' => 'Token expired, please login again',
|
||||||
|
'token 格式无效' => 'Token format invalid',
|
||||||
|
'请注册' => 'Please register',
|
||||||
|
'请重新登录' => 'Please login again',
|
||||||
|
'请重新登录(当前账号已在其他处登录)' => 'Please login again (account logged in elsewhere)',
|
||||||
|
'数据不存在' => 'Data not found',
|
||||||
|
'不能设置父级为自身' => 'Cannot set parent to self',
|
||||||
|
'该菜单下存在子菜单,请先删除子菜单' => 'This menu has sub-menus, please delete them first',
|
||||||
|
'导入文件错误,请上传正确的文件格式xlsx' => 'Import file error, please upload correct xlsx file',
|
||||||
|
'不能操作比当前账户职级高的角色' => 'Cannot operate roles with higher level than current account',
|
||||||
|
'该字典标识已存在' => 'This dict code already exists',
|
||||||
|
'修改数据异常,请检查' => 'Update data error, please check',
|
||||||
|
'删除数据异常,请检查' => 'Delete data error, please check',
|
||||||
|
'字典类型不存在' => 'Dict type not found',
|
||||||
|
'配置数据未找到' => 'Config data not found',
|
||||||
|
'系统默认分组,无法删除' => 'System default group cannot be deleted',
|
||||||
|
'配置组未找到' => 'Config group not found',
|
||||||
|
'上级分类和当前分类不能相同' => 'Parent category cannot be the same as current',
|
||||||
|
'不能将上级分类设置为当前分类的子分类' => 'Cannot set parent category as child of current',
|
||||||
|
'该部门下存在子分类,请先删除子分类' => 'This category has sub-categories, please delete them first',
|
||||||
|
'目标分类不存在' => 'Target category not found',
|
||||||
|
'获取文件资源失败' => 'Failed to get file resource',
|
||||||
|
'创建图片资源失败' => 'Failed to create image resource',
|
||||||
|
'文件格式错误' => 'Invalid file format',
|
||||||
|
'文件保存失败' => 'Failed to save file',
|
||||||
|
'当前表不支持回收站功能' => 'Current table does not support recycle bin',
|
||||||
|
'模板不存在' => 'Template not found',
|
||||||
|
'任务类型异常' => 'Invalid task type',
|
||||||
|
'数据库配置读取失败' => 'Failed to read database config',
|
||||||
|
'应用类型必须为plugin或者app' => 'App type must be plugin or app',
|
||||||
|
'请先设置应用名称' => 'Please set app name first',
|
||||||
|
'请选择要生成的表' => 'Please select tables to generate',
|
||||||
|
'非调试模式下,不允许生成文件' => 'File generation not allowed in non-debug mode',
|
||||||
|
'登录凭获取失败,请检查' => 'Failed to get login credential, please check',
|
||||||
|
'文件大小超过限制' => 'File size exceeds limit',
|
||||||
|
'不支持该格式的文件上传' => 'File format not supported for upload',
|
||||||
|
'该上传模式不存在' => 'Upload mode not found',
|
||||||
|
'切片上传服务必须在 HTTP 请求环境下调用' => 'Chunk upload must be called in HTTP request context',
|
||||||
|
'切片文件查找失败,请重新上传' => 'Chunk file not found, please upload again',
|
||||||
|
'未设置邮件配置' => 'Mail config not set',
|
||||||
|
'请执行 composer require phpmailer/phpmailer 并重启' => 'Please run composer require phpmailer/phpmailer and restart',
|
||||||
|
'仅超级管理员能够操作' => 'Only super admin can perform this action',
|
||||||
|
'等待依赖安装' => 'Waiting for dependencies to be installed',
|
||||||
|
'插件目录不存在' => 'Plugin directory not found',
|
||||||
|
'该插件的基础配置信息不完善' => 'Plugin base config is incomplete',
|
||||||
|
'参数错误' => 'Invalid parameters',
|
||||||
|
'无法打开文件,或者文件创建失败' => 'Cannot open file or create file failed',
|
||||||
|
'系统生成文件错误' => 'System file generation error',
|
||||||
|
'模板目录不存在!' => 'Template directory not found',
|
||||||
|
'文件类型异常,无法生成指定文件!' => 'Invalid file type, cannot generate file',
|
||||||
|
'前端目录查找失败,必须与后端目录为同级目录!' => 'Frontend directory not found, must be same level as backend',
|
||||||
|
];
|
||||||
Reference in New Issue
Block a user