28 KiB
MemeMind 主玩法 API 协议文档
本文档面向微信小游戏客户端(Cocos Creator)开发人员,涵盖认证、用户体力、关卡闯关等核心玩法接口。
目录
概述
MemeMind 核心玩法接口分为以下模块:
| 模块 | 路由前缀 | 说明 |
|---|---|---|
| 认证 | /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/game-data |
GET /api/v1/user/game-data(路径不变,响应结构变更) |
GET /api/v1/wechat-game/levels |
GET /api/v1/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 |
认证方式
除微信登录和游戏配置接口外,所有接口均需通过 JWT Token 进行身份认证。
请求头格式
Authorization: Bearer <token>
token 为微信登录接口返回的 JWT 令牌,有效期 7 天。
通用响应格式
所有接口均返回以下 JSON 结构:
{
"success": true,
"data": { "..." : "..." },
"message": null,
"timestamp": "2026-04-10T12:00:00.000Z"
}
| 字段 | 类型 | 说明 |
|---|---|---|
| success | boolean | 请求是否成功 |
| data | T | null | 成功时返回业务数据,失败时为 null |
| message | string | null | 失败时的错误信息,成功时为 null |
| timestamp | string | 服务器响应时间(ISO 8601) |
体力值系统说明
体力值(stamina)替代了原有的积分系统,用于控制用户进入关卡的频率。
| 属性 | 值 |
|---|---|
| 默认体力 | 50(新用户注册时) |
| 上限 | 50 |
| 恢复速度 | 每 10 分钟 恢复 1 点 |
| 消耗 | 进入未通关关卡时消耗 1 点 |
| 已通关关卡 | 再次进入不消耗体力 |
体力值数据结构
接口中体力信息统一使用以下结构返回:
interface StaminaInfo {
current: number; // 当前体力值(已计算恢复)
max: number; // 体力上限,固定为 50
nextRecoverAt: string | null; // 下一点体力恢复的时间(ISO 8601),满体力时为 null
}
示例:
{
"current": 45,
"max": 50,
"nextRecoverAt": "2026-04-10T12:10:00.000Z"
}
注意:体力恢复为服务端实时计算,无需客户端轮询。每次调用包含体力信息的接口时,服务端都会返回最新的体力值。客户端可根据
nextRecoverAt自行做倒计时 UI 展示。
接口列表
1. 微信登录
获取用户身份令牌。
接口地址:POST /api/v1/auth/wx-login
是否需要认证:否
请求体:
{
"code": "微信 wx.login 返回的 code"
}
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| code | string | 是 | 微信 wx.login 返回的临时登录凭证 |
响应数据:
{
token: string;
user: {
id: string;
nickname: string | null;
stamina: number; // 当前体力值(数据库原始值,不含实时恢复计算)
}
}
成功响应示例:
{
"success": true,
"data": {
"token": "eyJhbGciOiJIUzI1NiIs...",
"user": {
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"nickname": null,
"stamina": 50
}
},
"message": null,
"timestamp": "2026-04-10T12:00:00.000Z"
}
客户端调用时机:
- 小游戏冷启动时
- 缓存的 token 过期后(收到 401 响应时)
2. 获取用户资料
获取当前用户的资料,包括实时计算后的体力值。
接口地址:GET /api/v1/user/profile
是否需要认证:是
请求参数:无
响应数据:
{
id: string;
nickname: string | null;
stamina: StaminaInfo; // 实时体力信息
}
成功响应示例:
{
"success": true,
"data": {
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"nickname": null,
"stamina": {
"current": 45,
"max": 50,
"nextRecoverAt": "2026-04-10T12:10:00.000Z"
}
},
"message": null,
"timestamp": "2026-04-10T12:00:00.000Z"
}
客户端调用时机:
- 需要刷新用户体力显示时
- 从后台切回前台时
3. 获取游戏数据
获取用户体力值和通关进度,适用于游戏 Loading 页面一次性加载。
接口地址:GET /api/v1/user/game-data
是否需要认证:是
请求参数:无
响应数据:
{
user: {
id: string;
stamina: StaminaInfo;
};
completedLevelIds: string[]; // 已通关的关卡 ID 列表
}
成功响应示例:
{
"success": true,
"data": {
"user": {
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"stamina": {
"current": 50,
"max": 50,
"nextRecoverAt": null
}
},
"completedLevelIds": ["level_001", "level_002", "level_005"]
},
"message": null,
"timestamp": "2026-04-10T12:00:00.000Z"
}
客户端调用时机:
- 游戏 Loading 页面
- 进入关卡选择页面前
4. 获取关卡列表
获取所有关卡列表。已通关的关卡返回答案、谐音梗说明和线索,未通关的关卡不返回敏感数据。
接口地址:GET /api/v1/levels
是否需要认证:是
请求参数:无
响应数据:
{
levels: LevelListItem[];
total: number;
}
interface LevelListItem {
id: string; // 关卡 ID
level: number; // 关卡编号(从 1 开始)
image1Url: string; // 图片1 URL
image1Description: string | null; // 图片1 文本说明
image2Url: string; // 图片2 URL
image2Description: string | null; // 图片2 文本说明
answer: string | null; // 答案(仅已通关时返回,否则 null)
punchline: string | null; // 谐音梗说明(仅已通关时返回,否则 null)
hint1: string | null; // 线索1(仅已通关时返回,否则 null)
hint2: string | null; // 线索2(仅已通关时返回,否则 null)
hint3: string | null; // 线索3(仅已通关时返回,否则 null)
completed: boolean; // 是否已通关
timeSpent: number | null; // 通关时长(秒),未通关时为 null
}
成功响应示例:
{
"success": true,
"data": {
"levels": [
{
"id": "level_001",
"level": 1,
"image1Url": "https://cdn.example.com/levels/001_1.png",
"image1Description": "一只猫在看鱼",
"image2Url": "https://cdn.example.com/levels/001_2.png",
"image2Description": "一条鱼在飞",
"answer": "梗答案",
"punchline": "谐音梗:鱼和猫的故事",
"hint1": "这是一个经典的...",
"hint2": "和某个明星有关",
"hint3": null,
"completed": true,
"timeSpent": 45
},
{
"id": "level_002",
"level": 2,
"image1Url": "https://cdn.example.com/levels/002_1.png",
"image1Description": "一个人在走路",
"image2Url": "https://cdn.example.com/levels/002_2.png",
"image2Description": "一辆车在跑",
"answer": null,
"punchline": null,
"hint1": null,
"hint2": null,
"hint3": null,
"completed": false,
"timeSpent": null
}
],
"total": 2
},
"message": null,
"timestamp": "2026-04-10T12:00:00.000Z"
}
客户端使用说明:
- 关卡选择页面使用此接口获取关卡列表
- 每个关卡有两张图片(
image1Url、image2Url)和对应的文本说明 - 根据
completed字段展示不同的 UI 状态(已通关/未通关) - 未通关关卡的
answer、punchline、hint1、hint2、hint3均为null,客户端不应缓存这些字段
5. 进入关卡
消耗 1 点体力进入关卡,获取完整的关卡详情(含答案和线索)。
接口地址:POST /api/v1/levels/{id}/enter
是否需要认证:是
路径参数:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| id | string | 是 | 关卡 ID |
请求体:无
响应数据:
{
id: string;
level: number;
image1Url: string;
image1Description: string | null;
image2Url: string;
image2Description: string | null;
answer: string;
punchline: string | null;
hint1: string | null;
hint2: string | null;
hint3: string | null;
stamina: StaminaInfo; // 消耗后的体力信息
}
成功响应示例:
{
"success": true,
"data": {
"id": "level_002",
"level": 2,
"image1Url": "https://cdn.example.com/levels/002_1.png",
"image1Description": "一个人在走路",
"image2Url": "https://cdn.example.com/levels/002_2.png",
"image2Description": "一辆车在跑",
"answer": "这是答案",
"punchline": "谐音梗:走和跑的故事",
"hint1": "第一个线索",
"hint2": "第二个线索",
"hint3": null,
"stamina": {
"current": 47,
"max": 50,
"nextRecoverAt": "2026-04-10T12:10:00.000Z"
}
},
"message": null,
"timestamp": "2026-04-10T12:00:00.000Z"
}
业务逻辑:
| 场景 | 是否消耗体力 | 说明 |
|---|---|---|
| 首次进入未通关关卡 | ✅ 消耗 1 点 | 正常扣减 |
| 再次进入已通关关卡 | ❌ 不消耗 | 直接返回关卡详情 |
| 体力为 0 且关卡未通关 | ❌ 返回错误 | 返回 401 体力不足 |
客户端调用时机:
- 用户在关卡选择页面点击某个关卡进入时
- 必须调用此接口获取关卡详情后才能开始游戏
- 客户端应在调用前检查体力是否足够,体力不足时提示用户等待恢复
6. 通关上报
用户通关后上报通关时长。同一关卡不会重复记录。
接口地址:POST /api/v1/levels/{id}/complete
是否需要认证:是
路径参数:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| id | string | 是 | 关卡 ID |
请求体:
{
"timeSpent": 45
}
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| timeSpent | number | 是 | 通关时长(秒),≥ 0 |
响应数据:
{
firstClear: boolean; // 是否为首次通关
levelId: string; // 关卡 ID
timeSpent: number; // 记录的通关时长(秒)
}
成功响应示例(首次通关):
{
"success": true,
"data": {
"firstClear": true,
"levelId": "level_002",
"timeSpent": 45
},
"message": null,
"timestamp": "2026-04-10T12:00:00.000Z"
}
成功响应示例(重复通关):
{
"success": true,
"data": {
"firstClear": false,
"levelId": "level_002",
"timeSpent": 30
},
"message": null,
"timestamp": "2026-04-10T12:00:00.000Z"
}
业务逻辑:
- 首次通关:记录
timeSpent,返回firstClear: true - 重复通关:不覆盖记录,返回首次通关的
timeSpent,firstClear: false
客户端调用时机:
- 用户成功回答正确答案后调用
- 只在通关成功时调用,答错不需要上报
7. 获取游戏配置
获取所有激活的游戏配置。
接口地址:GET /api/v1/game-configs
是否需要认证:否
请求参数:无
响应数据:
{
configs: GameConfig[];
total: number;
}
interface GameConfig {
id: string;
configKey: string;
configValue: string;
description: string | null;
isActive: boolean;
createdAt: string;
updatedAt: string;
}
成功响应示例:
{
"success": true,
"data": {
"configs": [
{
"id": "cfg-001",
"configKey": "hint_unlock_cost",
"configValue": "1",
"description": "解锁提示消耗体力值",
"isActive": true,
"createdAt": "2026-04-01T00:00:00.000Z",
"updatedAt": "2026-04-01T00:00:00.000Z"
}
],
"total": 1
},
"message": null,
"timestamp": "2026-04-10T12:00:00.000Z"
}
8. 获取单个游戏配置
根据配置 key 获取单个配置。
接口地址:GET /api/v1/game-configs/{key}
是否需要认证:否
路径参数:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| key | string | 是 | 配置键名 |
响应数据:
{
id: string;
configKey: string;
configValue: string;
description: string | null;
isActive: boolean;
createdAt: string;
updatedAt: string;
}
错误码说明
| 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 不存在 |
接入流程
核心游戏流程
┌──────────────────────────────────────────────────────────────────────┐
│ Phase 1: 启动 & 登录 │
├──────────────────────────────────────────────────────────────────────┤
│ 1. 小游戏启动,调用 wx.login 获取 code │
│ 2. POST /api/v1/auth/wx-login → 获取 JWT token │
│ 3. 缓存 token 到本地 │
└──────────────────────┬───────────────────────────────────────────────┘
▼
┌──────────────────────────────────────────────────────────────────────┐
│ Phase 2: Loading 页面 │
├──────────────────────────────────────────────────────────────────────┤
│ 4. GET /api/v1/user/game-data → 获取体力值 + 已通关关卡列表 │
│ 5. GET /api/v1/game-configs → 获取游戏配置(可选) │
└──────────────────────┬───────────────────────────────────────────────┘
▼
┌──────────────────────────────────────────────────────────────────────┐
│ Phase 3: 关卡选择页面 │
├──────────────────────────────────────────────────────────────────────┤
│ 6. GET /api/v1/levels → 获取关卡列表(含通关状态) │
│ 7. 展示关卡网格,已通关的显示已完成状态 │
│ 8. 用户点击某个关卡 │
│ ├─ 体力足够 → Phase 4 │
│ └─ 体力不足 → 提示等待恢复(显示 nextRecoverAt 倒计时) │
└──────────────────────┬───────────────────────────────────────────────┘
▼
┌──────────────────────────────────────────────────────────────────────┐
│ Phase 4: 关卡游玩 │
├──────────────────────────────────────────────────────────────────────┤
│ 9. POST /api/v1/levels/{id}/enter → 消耗体力,获取关卡详情 │
│ 10. 展示关卡图片,开始计时 │
│ 11. 用户输入答案 │
│ ├─ 答案正确 → 停止计时 │
│ │ └─ POST /api/v1/levels/{id}/complete → 上报通关时长 │
│ └─ 答案错误 → 提示错误,可使用线索 │
└──────────────────────────────────────────────────────────────────────┘
客户端状态管理建议
interface GameState {
// 用户信息
token: string | null;
userId: string | null;
// 体力信息
stamina: {
current: number;
max: number;
nextRecoverAt: string | null;
};
// 关卡数据
completedLevelIds: Set<string>; // 已通关关卡 ID
currentLevel: { // 当前正在玩的关卡
id: string;
answer: string;
punchline: string | null;
hints: (string | null)[];
images: { url: string; description: string | null }[];
startTime: number; // 开始时间戳,用于计算 timeSpent
} | null;
}
体力恢复倒计时实现建议
// 客户端体力恢复倒计时(纯 UI 展示)
function startStaminaTimer(staminaInfo: StaminaInfo) {
if (!staminaInfo.nextRecoverAt || staminaInfo.current >= staminaInfo.max) {
// 满体力,无需倒计时
return;
}
const targetTime = new Date(staminaInfo.nextRecoverAt).getTime();
const timer = setInterval(() => {
const remaining = targetTime - Date.now();
if (remaining <= 0) {
// 恢复一点体力(本地模拟)
staminaInfo.current = Math.min(staminaInfo.current + 1, staminaInfo.max);
clearInterval(timer);
if (staminaInfo.current < staminaInfo.max) {
// 继续下一轮倒计时
staminaInfo.nextRecoverAt = new Date(Date.now() + 10 * 60 * 1000).toISOString();
startStaminaTimer(staminaInfo);
} else {
staminaInfo.nextRecoverAt = null;
}
} else {
// 更新倒计时 UI
const minutes = Math.floor(remaining / 60000);
const seconds = Math.floor((remaining % 60000) / 1000);
updateStaminaUI(`${minutes}:${seconds.toString().padStart(2, '0')}`);
}
}, 1000);
}
⚠️ 客户端体力倒计时仅用于 UI 展示。实际体力值以服务端返回为准,每次调用接口时会获得最新体力。
Cocos Creator 调用示例
1. HTTP 请求工具类
// HttpManager.ts
export interface ApiResponse<T> {
success: boolean;
data: T | null;
message: string | null;
timestamp: string;
}
export interface StaminaInfo {
current: number;
max: number;
nextRecoverAt: string | null;
}
class HttpManager {
private baseUrl = 'https://your-api-domain.com/api';
private token: string | null = null;
setToken(token: string) {
this.token = token;
}
getToken(): string | null {
return this.token;
}
async request<T>(
method: 'GET' | 'POST',
url: string,
body?: object
): Promise<ApiResponse<T>> {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open(method, this.baseUrl + url, true);
xhr.setRequestHeader('Content-Type', 'application/json');
if (this.token) {
xhr.setRequestHeader('Authorization', `Bearer ${this.token}`);
}
xhr.onload = () => {
try {
const resp = JSON.parse(xhr.responseText);
if (xhr.status >= 200 && xhr.status < 300) {
resolve(resp);
} else if (xhr.status === 401) {
// Token 过期,触发重新登录
this.token = null;
reject(new Error(resp.message || '登录已过期,请重新登录'));
} else {
reject(new Error(resp.message || `请求失败: ${xhr.status}`));
}
} catch {
reject(new Error(`请求失败: ${xhr.status}`));
}
};
xhr.onerror = () => reject(new Error('网络错误'));
xhr.send(body ? JSON.stringify(body) : undefined);
});
}
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();
2. 微信登录
async function wxLogin() {
const { code } = await new Promise<{ code: string }>((resolve, reject) => {
wx.login({ success: (res) => resolve({ code: res.code }), fail: reject });
});
const resp = await http.post<{
token: string;
user: { id: string; nickname: string | null; stamina: number };
}>('/v1/auth/wx-login', { code });
if (resp.success && resp.data) {
http.setToken(resp.data.token);
wx.setStorageSync('jwt_token', resp.data.token);
return resp.data;
}
throw new Error(resp.message || '登录失败');
}
3. Loading 页面加载游戏数据
interface GameData {
user: { id: string; stamina: StaminaInfo };
completedLevelIds: string[];
}
async function loadGameData(): Promise<GameData> {
const resp = await http.get<GameData>('/v1/user/game-data');
if (resp.success && resp.data) {
return resp.data;
}
throw new Error(resp.message || '加载游戏数据失败');
}
4. 获取关卡列表
interface LevelListItem {
id: string;
level: number;
image1Url: string;
image1Description: string | null;
image2Url: string;
image2Description: string | null;
answer: string | null;
punchline: string | null;
hint1: string | null;
hint2: string | null;
hint3: string | null;
completed: boolean;
timeSpent: number | null;
}
async function getLevels(): Promise<LevelListItem[]> {
const resp = await http.get<{ levels: LevelListItem[]; total: number }>('/v1/levels');
if (resp.success && resp.data) {
return resp.data.levels;
}
throw new Error(resp.message || '获取关卡列表失败');
}
5. 进入关卡(核心接口)
interface EnterLevelResponse {
id: string;
level: number;
image1Url: string;
image1Description: string | null;
image2Url: string;
image2Description: string | null;
answer: string;
punchline: string | null;
hint1: string | null;
hint2: string | null;
hint3: string | null;
stamina: StaminaInfo;
}
async function enterLevel(levelId: string): Promise<EnterLevelResponse> {
const resp = await http.post<EnterLevelResponse>(`/v1/levels/${levelId}/enter`);
if (resp.success && resp.data) {
// 更新本地体力状态
updateLocalStamina(resp.data.stamina);
return resp.data;
}
throw new Error(resp.message || '进入关卡失败');
}
// 调用示例
async function onClickLevel(levelId: string, currentStamina: number, isCompleted: boolean) {
if (!isCompleted && currentStamina <= 0) {
showToast('体力不足,请等待恢复');
return;
}
try {
const levelData = await enterLevel(levelId);
// 跳转到游戏页面,传入关卡数据
startGame(levelData);
} catch (err) {
showToast(err.message);
}
}
6. 通关上报
interface CompleteLevelResponse {
firstClear: boolean;
levelId: string;
timeSpent: number;
}
async function completeLevel(
levelId: string,
timeSpent: number
): Promise<CompleteLevelResponse> {
const resp = await http.post<CompleteLevelResponse>(
`/v1/levels/${levelId}/complete`,
{ timeSpent }
);
if (resp.success && resp.data) {
if (resp.data.firstClear) {
showToast('恭喜通关!');
}
return resp.data;
}
throw new Error(resp.message || '上报通关失败');
}
// 调用示例:用户答对时
async function onAnswerCorrect(levelId: string, startTime: number) {
const timeSpent = Math.floor((Date.now() - startTime) / 1000);
const result = await completeLevel(levelId, timeSpent);
// 更新关卡选择页面的状态
}
7. 完整启动流程
// GameEntry.ts
import { _decorator, Component } from 'cc';
const { ccclass } = _decorator;
@ccclass('GameEntry')
export class GameEntry extends Component {
async start() {
try {
// 1. 尝试使用缓存 token
const cachedToken = wx.getStorageSync('jwt_token');
if (cachedToken) {
http.setToken(cachedToken);
}
// 2. 登录(获取最新 token)
const loginData = await wxLogin();
console.log('登录成功:', loginData.user.id);
// 3. 加载游戏数据
const gameData = await loadGameData();
console.log('体力:', gameData.user.stamina.current);
console.log('已通关:', gameData.completedLevelIds.length, '关');
// 4. 启动体力恢复倒计时
startStaminaTimer(gameData.user.stamina);
// 5. 获取关卡列表,进入关卡选择页面
const levels = await getLevels();
showLevelSelect(levels);
} catch (error) {
console.error('启动失败:', error);
showRetryDialog();
}
}
}
注意事项
- Token 有效期:JWT Token 有效期 7 天,建议本地缓存并在启动时尝试复用
- 体力值以服务端为准:客户端倒计时仅为 UI 展示,实际体力以接口返回为准
- 进入关卡必须调用接口:不要使用列表接口中缓存的关卡数据直接开始游戏,必须调用
enter接口 - 已通关关卡免费进入:已通关关卡再次进入不消耗体力
- 通关上报仅限成功:只在用户答对后调用
complete接口,答错不需要上报 - hint 字段:
hint1/hint2/hint3可能为null,表示该线索未配置 - punchline 字段:谐音梗说明,仅已通关时返回,未通关时为
null - 双图结构:每个关卡有两张图片(
image1Url、image2Url),分别有对应的文本说明 - 网络异常处理:建议所有接口调用加 loading 状态,并处理 401(重新登录)和网络错误