fix: 优化关卡排序
This commit is contained in:
@@ -26,8 +26,11 @@
|
||||
|
||||
## Changelog
|
||||
|
||||
- **2026-05-03**:
|
||||
- **变更** 关卡顺序规则:所有下一关、预加载下一关、已通关列表均按 `levels.sort_key` 的应用层字节序排序;响应中的 `level` / `sortOrder` 仍为排序后的连续序号
|
||||
|
||||
- **2026-04-30**:
|
||||
- **新增** `GET /api/v1/levels/completed`:获取当前用户已通关关卡列表(按 sortOrder 升序,含完整关卡信息 + 通关时长 + 通关时间)
|
||||
- **新增** `GET /api/v1/levels/completed`:获取当前用户已通关关卡列表(含完整关卡信息 + 通关时长 + 通关时间)
|
||||
|
||||
- **2026-04-26**:
|
||||
- **删除** `GET /api/v1/levels`(关卡列表接口),客户端不再需要拉取全量关卡列表
|
||||
@@ -41,26 +44,26 @@
|
||||
|
||||
MemeMind 核心玩法接口分为以下模块:
|
||||
|
||||
| 模块 | 路由前缀 | 说明 |
|
||||
|------|----------|------|
|
||||
| 认证 | `/api/v1/auth` | 微信登录、JWT 签发 |
|
||||
| 用户 | `/api/v1/user` | 用户资料、体力值、游戏数据 |
|
||||
| 关卡 | `/api/v1/levels` | 进入关卡、通关上报 |
|
||||
| 游戏配置 | `/api/v1/game-configs` | 游戏全局配置 |
|
||||
| 模块 | 路由前缀 | 说明 |
|
||||
| -------- | ---------------------- | -------------------------- |
|
||||
| 认证 | `/api/v1/auth` | 微信登录、JWT 签发 |
|
||||
| 用户 | `/api/v1/user` | 用户资料、体力值、游戏数据 |
|
||||
| 关卡 | `/api/v1/levels` | 进入关卡、通关上报 |
|
||||
| 游戏配置 | `/api/v1/game-configs` | 游戏全局配置 |
|
||||
|
||||
### 与旧版 API 的变更(⚠️ Breaking Changes)
|
||||
|
||||
| 废弃接口 | 替代方案 |
|
||||
|----------|---------|
|
||||
| `GET /api/v1/user/assets` | `GET /api/v1/user/profile` |
|
||||
| `POST /api/v1/user/assets/consume` | 已删除,体力在「进入关卡」时自动扣减 |
|
||||
| `POST /api/v1/user/assets/earn` | 已删除,通关不再奖励积分 |
|
||||
| 废弃接口 | 替代方案 |
|
||||
| ---------------------------------------------------------- | ------------------------------------------------------------------------ |
|
||||
| `GET /api/v1/user/assets` | `GET /api/v1/user/profile` |
|
||||
| `POST /api/v1/user/assets/consume` | 已删除,体力在「进入关卡」时自动扣减 |
|
||||
| `POST /api/v1/user/assets/earn` | 已删除,通关不再奖励积分 |
|
||||
| `GET /api/v1/user/game-data`(旧版返回 completedLevelIds) | `GET /api/v1/user/game-data`(新版返回 completedLevelCount + nextLevel) |
|
||||
| `GET /api/v1/levels` | 已删除,下一关数据由 `game-data` / `enter` / `complete` 接口直接返回 |
|
||||
| `GET /api/v1/wechat-game/levels` | 已删除 |
|
||||
| `GET /api/v1/wechat-game/levels/:id` | `POST /api/v1/levels/:id/enter`(需鉴权 + 消耗体力) |
|
||||
| `GET /api/v1/wechat-game/configs` | `GET /api/v1/game-configs` |
|
||||
| `GET /api/v1/wechat-game/configs/:key` | `GET /api/v1/game-configs/:key` |
|
||||
| `GET /api/v1/levels` | 已删除,下一关数据由 `game-data` / `enter` / `complete` 接口直接返回 |
|
||||
| `GET /api/v1/wechat-game/levels` | 已删除 |
|
||||
| `GET /api/v1/wechat-game/levels/:id` | `POST /api/v1/levels/:id/enter`(需鉴权 + 消耗体力) |
|
||||
| `GET /api/v1/wechat-game/configs` | `GET /api/v1/game-configs` |
|
||||
| `GET /api/v1/wechat-game/configs/:key` | `GET /api/v1/game-configs/:key` |
|
||||
|
||||
---
|
||||
|
||||
@@ -85,18 +88,18 @@ Authorization: Bearer <token>
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": { "..." : "..." },
|
||||
"data": { "...": "..." },
|
||||
"message": null,
|
||||
"timestamp": "2026-04-10T12:00:00.000Z"
|
||||
}
|
||||
```
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| success | boolean | 请求是否成功 |
|
||||
| data | T \| null | 成功时返回业务数据,失败时为 null |
|
||||
| message | string \| null | 失败时的错误信息,成功时为 null |
|
||||
| timestamp | string | 服务器响应时间(ISO 8601) |
|
||||
| 字段 | 类型 | 说明 |
|
||||
| --------- | -------------- | --------------------------------- |
|
||||
| success | boolean | 请求是否成功 |
|
||||
| data | T \| null | 成功时返回业务数据,失败时为 null |
|
||||
| message | string \| null | 失败时的错误信息,成功时为 null |
|
||||
| timestamp | string | 服务器响应时间(ISO 8601) |
|
||||
|
||||
---
|
||||
|
||||
@@ -104,13 +107,13 @@ Authorization: Bearer <token>
|
||||
|
||||
体力值(stamina)替代了原有的积分系统,用于控制用户进入关卡的频率。
|
||||
|
||||
| 属性 | 值 |
|
||||
|------|-----|
|
||||
| 默认体力 | 50(新用户注册时) |
|
||||
| 上限 | 50 |
|
||||
| 恢复速度 | 每 **10 分钟** 恢复 1 点 |
|
||||
| 消耗 | 进入**未通关**关卡时消耗 1 点 |
|
||||
| 已通关关卡 | 再次进入不消耗体力 |
|
||||
| 属性 | 值 |
|
||||
| ---------- | ----------------------------- |
|
||||
| 默认体力 | 50(新用户注册时) |
|
||||
| 上限 | 50 |
|
||||
| 恢复速度 | 每 **10 分钟** 恢复 1 点 |
|
||||
| 消耗 | 进入**未通关**关卡时消耗 1 点 |
|
||||
| 已通关关卡 | 再次进入不消耗体力 |
|
||||
|
||||
### 体力值数据结构
|
||||
|
||||
@@ -118,8 +121,8 @@ Authorization: Bearer <token>
|
||||
|
||||
```typescript
|
||||
interface StaminaInfo {
|
||||
current: number; // 当前体力值(已计算恢复)
|
||||
max: number; // 体力上限,固定为 50
|
||||
current: number; // 当前体力值(已计算恢复)
|
||||
max: number; // 体力上限,固定为 50
|
||||
nextRecoverAt: string | null; // 下一点体力恢复的时间(ISO 8601),满体力时为 null
|
||||
}
|
||||
```
|
||||
@@ -146,22 +149,23 @@ interface StaminaInfo {
|
||||
|
||||
```typescript
|
||||
interface NextLevel {
|
||||
id: string; // 关卡 ID
|
||||
level: number; // 关卡编号(sortOrder)
|
||||
image1Url: string; // 图片1 URL
|
||||
id: string; // 关卡 ID
|
||||
level: number; // 关卡编号(sortOrder)
|
||||
image1Url: string; // 图片1 URL
|
||||
image1Description: string | null; // 图片1 文本说明
|
||||
image2Url: string; // 图片2 URL
|
||||
image2Url: string; // 图片2 URL
|
||||
image2Description: string | null; // 图片2 文本说明
|
||||
answer: string; // 答案
|
||||
punchline: string | null; // 谐音梗说明
|
||||
hint1: string | null; // 线索1
|
||||
hint2: string | null; // 线索2
|
||||
hint3: string | null; // 线索3
|
||||
timeLimit: number | null; // 限时(秒),null 表示不限时
|
||||
answer: string; // 答案
|
||||
punchline: string | null; // 谐音梗说明
|
||||
hint1: string | null; // 线索1
|
||||
hint2: string | null; // 线索2
|
||||
hint3: string | null; // 线索3
|
||||
timeLimit: number | null; // 限时(秒),null 表示不限时
|
||||
}
|
||||
```
|
||||
|
||||
**出现位置**:
|
||||
|
||||
- `GET /api/v1/user/game-data` → `nextLevel`
|
||||
- `POST /api/v1/levels/:id/enter` → `preloadNextLevel`
|
||||
- `POST /api/v1/levels/:id/complete` → `nextLevel`
|
||||
@@ -186,9 +190,9 @@ interface NextLevel {
|
||||
}
|
||||
```
|
||||
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| code | string | 是 | 微信 `wx.login` 返回的临时登录凭证 |
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
| ---- | ------ | ---- | ---------------------------------- |
|
||||
| code | string | 是 | 微信 `wx.login` 返回的临时登录凭证 |
|
||||
|
||||
**响应数据**:
|
||||
|
||||
@@ -198,7 +202,7 @@ interface NextLevel {
|
||||
user: {
|
||||
id: string;
|
||||
nickname: string | null;
|
||||
stamina: number; // 当前体力值(数据库原始值,不含实时恢复计算)
|
||||
stamina: number; // 当前体力值(数据库原始值,不含实时恢复计算)
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -222,6 +226,7 @@ interface NextLevel {
|
||||
```
|
||||
|
||||
**客户端调用时机**:
|
||||
|
||||
- 小游戏冷启动时
|
||||
- 缓存的 token 过期后(收到 401 响应时)
|
||||
|
||||
@@ -243,7 +248,7 @@ interface NextLevel {
|
||||
{
|
||||
id: string;
|
||||
nickname: string | null;
|
||||
stamina: StaminaInfo; // 实时体力信息
|
||||
stamina: StaminaInfo; // 实时体力信息
|
||||
}
|
||||
```
|
||||
|
||||
@@ -267,6 +272,7 @@ interface NextLevel {
|
||||
```
|
||||
|
||||
**客户端调用时机**:
|
||||
|
||||
- 需要刷新用户体力显示时
|
||||
- 从后台切回前台时
|
||||
|
||||
@@ -289,9 +295,9 @@ interface NextLevel {
|
||||
user: {
|
||||
id: string;
|
||||
stamina: StaminaInfo;
|
||||
};
|
||||
completedLevelCount: number; // 已通关的关卡数量
|
||||
nextLevel: NextLevel | null; // 下一个待通关的关卡(全部通关时为 null)
|
||||
}
|
||||
completedLevelCount: number; // 已通关的关卡数量
|
||||
nextLevel: NextLevel | null; // 下一个待通关的关卡(全部通关时为 null)
|
||||
}
|
||||
```
|
||||
|
||||
@@ -349,10 +355,13 @@ interface NextLevel {
|
||||
```
|
||||
|
||||
**业务逻辑**:
|
||||
- `nextLevel` 为按 `sortOrder` 排序的第一个用户未通关的关卡
|
||||
|
||||
- `nextLevel` 为按 `levels.sort_key` 应用层字节序排序后的第一个用户未通关关卡
|
||||
- 响应中的 `nextLevel.level` 是按当前排序回填的 0-based 连续序号,客户端不需要感知数据库内部的 `sort_key`
|
||||
- 全部通关时 `nextLevel` 为 `null`,客户端应展示通关庆祝页面
|
||||
|
||||
**客户端调用时机**:
|
||||
|
||||
- 游戏 Loading 页面
|
||||
- 进入主页面前
|
||||
|
||||
@@ -368,9 +377,9 @@ interface NextLevel {
|
||||
|
||||
**路径参数**:
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| id | string | 是 | 关卡 ID |
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| ---- | ------ | ---- | ------- |
|
||||
| id | string | 是 | 关卡 ID |
|
||||
|
||||
**请求体**:无
|
||||
|
||||
@@ -389,7 +398,7 @@ interface NextLevel {
|
||||
hint1: string | null;
|
||||
hint2: string | null;
|
||||
hint3: string | null;
|
||||
stamina: StaminaInfo; // 消耗后的体力信息
|
||||
stamina: StaminaInfo; // 消耗后的体力信息
|
||||
preloadNextLevel: NextLevel | null; // 预加载的下一关数据(无下一关时为 null)
|
||||
}
|
||||
```
|
||||
@@ -438,17 +447,18 @@ interface NextLevel {
|
||||
|
||||
**业务逻辑**:
|
||||
|
||||
| 场景 | 是否消耗体力 | 说明 |
|
||||
|------|-------------|------|
|
||||
| 首次进入未通关关卡 | ✅ 消耗 1 点 | 正常扣减 |
|
||||
| 再次进入已通关关卡 | ❌ 不消耗 | 直接返回关卡详情 |
|
||||
| 体力为 0 且关卡未通关 | ❌ 返回错误 | 返回 401 体力不足 |
|
||||
| 场景 | 是否消耗体力 | 说明 |
|
||||
| --------------------- | ------------ | ----------------- |
|
||||
| 首次进入未通关关卡 | ✅ 消耗 1 点 | 正常扣减 |
|
||||
| 再次进入已通关关卡 | ❌ 不消耗 | 直接返回关卡详情 |
|
||||
| 体力为 0 且关卡未通关 | ❌ 返回错误 | 返回 401 体力不足 |
|
||||
|
||||
- `preloadNextLevel` 为按 `sortOrder` 排在当前关卡之后的第一个未完成关卡
|
||||
- 当前关卡是最后一关时,`preloadNextLevel` 为 `null`
|
||||
- 客户端可在用户答题时后台预加载 `preloadNextLevel` 中的图片资源
|
||||
|
||||
**客户端调用时机**:
|
||||
|
||||
- 用户在关卡选择页面点击某个关卡进入时
|
||||
- **必须**调用此接口获取关卡详情后才能开始游戏
|
||||
- 客户端应在调用前检查体力是否足够,体力不足时提示用户等待恢复
|
||||
@@ -465,9 +475,9 @@ interface NextLevel {
|
||||
|
||||
**路径参数**:
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| id | string | 是 | 关卡 ID |
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| ---- | ------ | ---- | ------- |
|
||||
| id | string | 是 | 关卡 ID |
|
||||
|
||||
**请求体**:
|
||||
|
||||
@@ -477,18 +487,18 @@ interface NextLevel {
|
||||
}
|
||||
```
|
||||
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| timeSpent | number | 是 | 通关时长(秒),≥ 0 |
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
| --------- | ------ | ---- | ------------------- |
|
||||
| timeSpent | number | 是 | 通关时长(秒),≥ 0 |
|
||||
|
||||
**响应数据**:
|
||||
|
||||
```typescript
|
||||
{
|
||||
firstClear: boolean; // 是否为首次通关
|
||||
levelId: string; // 关卡 ID
|
||||
timeSpent: number; // 记录的通关时长(秒)
|
||||
nextLevel: NextLevel | null; // 下一个待通关的关卡(全部通关时为 null)
|
||||
firstClear: boolean; // 是否为首次通关
|
||||
levelId: string; // 关卡 ID
|
||||
timeSpent: number; // 记录的通关时长(秒)
|
||||
nextLevel: NextLevel | null; // 下一个待通关的关卡(全部通关时为 null)
|
||||
}
|
||||
```
|
||||
|
||||
@@ -567,12 +577,14 @@ interface NextLevel {
|
||||
```
|
||||
|
||||
**业务逻辑**:
|
||||
|
||||
- 首次通关:记录 `timeSpent`,返回 `firstClear: true`
|
||||
- 重复通关:不覆盖记录,返回首次通关的 `timeSpent`,`firstClear: false`
|
||||
- `nextLevel` 为通关后按 `sortOrder` 排序的第一个未完成关卡
|
||||
- 全部通关时 `nextLevel` 为 `null`
|
||||
|
||||
**客户端调用时机**:
|
||||
|
||||
- 用户成功回答正确答案后调用
|
||||
- 只在通关成功时调用,答错不需要上报
|
||||
- 收到响应后,可直接使用 `nextLevel` 数据进入下一关(调用 `enter` 接口)
|
||||
@@ -668,11 +680,14 @@ interface CompletedLevel {
|
||||
```
|
||||
|
||||
**业务逻辑**:
|
||||
- 返回列表按关卡顺序(`sortOrder`)升序排列
|
||||
|
||||
- 返回列表按 `levels.sort_key` 应用层字节序升序排列
|
||||
- 响应中的 `level` 是按当前排序回填的 0-based 连续序号,客户端不需要感知数据库内部的 `sort_key`
|
||||
- `timeSpent` 为**首次通关**时上报的时长,重复通关不会覆盖
|
||||
- 每项包含完整关卡信息,客户端可直接在回看页面渲染图片、答案、线索
|
||||
|
||||
**客户端调用时机**:
|
||||
|
||||
- 用户打开「关卡回看 / 成就墙」页面时
|
||||
- 不建议在 Loading 阶段调用(Loading 使用 `game-data` 即可)
|
||||
|
||||
@@ -743,9 +758,9 @@ interface GameConfig {
|
||||
|
||||
**路径参数**:
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| key | string | 是 | 配置键名 |
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| ---- | ------ | ---- | -------- |
|
||||
| key | string | 是 | 配置键名 |
|
||||
|
||||
**响应数据**:
|
||||
|
||||
@@ -765,15 +780,15 @@ interface GameConfig {
|
||||
|
||||
## 错误码说明
|
||||
|
||||
| HTTP Status | message | 说明 |
|
||||
|-------------|---------|------|
|
||||
| 401 | 未提供访问令牌 | 请求头缺少 Authorization |
|
||||
| 401 | 访问令牌无效或已过期 | JWT Token 无效或过期,需重新登录 |
|
||||
| 401 | 微信登录失败,请重试 | 微信 code 无效 |
|
||||
| 401 | 用户不存在 | 用户 ID 在数据库中不存在 |
|
||||
| 401 | 体力不足 | 进入关卡时体力为 0 |
|
||||
| 404 | 关卡 {id} 不存在 | 关卡 ID 不存在 |
|
||||
| 404 | Game config with key "xxx" not found | 配置 key 不存在 |
|
||||
| HTTP Status | message | 说明 |
|
||||
| ----------- | ------------------------------------ | -------------------------------- |
|
||||
| 401 | 未提供访问令牌 | 请求头缺少 Authorization |
|
||||
| 401 | 访问令牌无效或已过期 | JWT Token 无效或过期,需重新登录 |
|
||||
| 401 | 微信登录失败,请重试 | 微信 code 无效 |
|
||||
| 401 | 用户不存在 | 用户 ID 在数据库中不存在 |
|
||||
| 401 | 体力不足 | 进入关卡时体力为 0 |
|
||||
| 404 | 关卡 {id} 不存在 | 关卡 ID 不存在 |
|
||||
| 404 | Game config with key "xxx" not found | 配置 key 不存在 |
|
||||
|
||||
---
|
||||
|
||||
@@ -848,13 +863,13 @@ interface GameState {
|
||||
completedLevelCount: number;
|
||||
|
||||
// 当前关卡
|
||||
currentLevel: NextLevel | null; // 来自 game-data.nextLevel 或 complete.nextLevel
|
||||
currentLevel: NextLevel | null; // 来自 game-data.nextLevel 或 complete.nextLevel
|
||||
|
||||
// 预加载的下一关
|
||||
preloadedLevel: NextLevel | null; // 来自 enter.preloadNextLevel
|
||||
|
||||
// 游戏中状态
|
||||
startTime: number | null; // 开始时间戳,用于计算 timeSpent
|
||||
startTime: number | null; // 开始时间戳,用于计算 timeSpent
|
||||
}
|
||||
```
|
||||
|
||||
@@ -879,7 +894,9 @@ function startStaminaTimer(staminaInfo: StaminaInfo) {
|
||||
|
||||
if (staminaInfo.current < staminaInfo.max) {
|
||||
// 继续下一轮倒计时
|
||||
staminaInfo.nextRecoverAt = new Date(Date.now() + 10 * 60 * 1000).toISOString();
|
||||
staminaInfo.nextRecoverAt = new Date(
|
||||
Date.now() + 10 * 60 * 1000,
|
||||
).toISOString();
|
||||
startStaminaTimer(staminaInfo);
|
||||
} else {
|
||||
staminaInfo.nextRecoverAt = null;
|
||||
@@ -947,7 +964,7 @@ class HttpManager {
|
||||
async request<T>(
|
||||
method: 'GET' | 'POST',
|
||||
url: string,
|
||||
body?: object
|
||||
body?: object,
|
||||
): Promise<ApiResponse<T>> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const xhr = new XMLHttpRequest();
|
||||
@@ -980,8 +997,12 @@ class HttpManager {
|
||||
});
|
||||
}
|
||||
|
||||
get<T>(url: string) { return this.request<T>('GET', url); }
|
||||
post<T>(url: string, body?: object) { return this.request<T>('POST', url, body); }
|
||||
get<T>(url: string) {
|
||||
return this.request<T>('GET', url);
|
||||
}
|
||||
post<T>(url: string, body?: object) {
|
||||
return this.request<T>('POST', url, body);
|
||||
}
|
||||
}
|
||||
|
||||
export const http = new HttpManager();
|
||||
@@ -1047,7 +1068,9 @@ interface EnterLevelResponse {
|
||||
}
|
||||
|
||||
async function enterLevel(levelId: string): Promise<EnterLevelResponse> {
|
||||
const resp = await http.post<EnterLevelResponse>(`/v1/levels/${levelId}/enter`);
|
||||
const resp = await http.post<EnterLevelResponse>(
|
||||
`/v1/levels/${levelId}/enter`,
|
||||
);
|
||||
|
||||
if (resp.success && resp.data) {
|
||||
// 更新本地体力状态
|
||||
@@ -1079,11 +1102,11 @@ interface CompleteLevelResponse {
|
||||
|
||||
async function completeLevel(
|
||||
levelId: string,
|
||||
timeSpent: number
|
||||
timeSpent: number,
|
||||
): Promise<CompleteLevelResponse> {
|
||||
const resp = await http.post<CompleteLevelResponse>(
|
||||
`/v1/levels/${levelId}/complete`,
|
||||
{ timeSpent }
|
||||
{ timeSpent },
|
||||
);
|
||||
|
||||
if (resp.success && resp.data) {
|
||||
@@ -1149,7 +1172,6 @@ export class GameEntry extends Component {
|
||||
// 全部通关
|
||||
showCelebration();
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('启动失败:', error);
|
||||
showRetryDialog();
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
2. **单关进度上报**:`POST /api/v1/share/progress` 接口,用于上报用户单关通关状态和时间
|
||||
3. **进度查询**:`reportLevelProgress` 返回是否在时间限制内通过
|
||||
4. **我创建的挑战列表**:`GET /api/v1/share/created` 接口,用于查询当前用户创建过的分享挑战、参与人数和本人排名
|
||||
5. **关卡排序**:关卡全局顺序按 `levels.sort_key` 的应用层字节序计算,接口中的 `sortOrder` 为排序后的 0-based 连续序号
|
||||
|
||||
---
|
||||
|
||||
@@ -273,6 +274,7 @@ Content-Type: application/json
|
||||
**特殊逻辑**:
|
||||
|
||||
- 如果 `userId` 与分享创建者相同,不会创建 `ShareParticipant` 记录
|
||||
- `levels` 数组顺序保持分享创建时传入的 `levelIds` 顺序;每个关卡的 `sortOrder` 字段为按 `levels.sort_key` 全局排序后回填的 0-based 连续序号
|
||||
- 返回的关卡列表按 `levelIds` 创建时的顺序排列
|
||||
|
||||
**客户端调用场景**:
|
||||
|
||||
Reference in New Issue
Block a user