消消乐引擎 对接文档
一套可换皮的 HTML5 三消游戏。买家(租户)通过 API 给自己的玩家发道具,在管理后台上传自己的图片包。
| 服务 | 地址 |
|---|---|
| 道具 API | https://xxl-api-test.htdtek.com |
| 管理后台 | https://xxl-admin-test.htdtek.com |
| 游戏 | https://xxl-classic-test.htdtek.com/?tenant=<租户>&player=<玩家ID> |
sk_xxx(secret key)、pk_xxx(publishable key)。测试用的真实 Key 由管理员单独发放,请勿贴到群聊、工单或截图里。测试团队使用租户 qa(新玩家初始道具:锤子、洗牌、加步各 3 个)。1. 5 分钟快速开始
① 设置环境变量(把管理员发给你的 Key 填进去):
export API=https://xxl-api-test.htdtek.com
export SK=sk_xxx # secret key,只能放在你的服务端
export PK=pk_xxx # publishable key,可以放在前端
export P=qa-test-$(date +%s) # 一个新的玩家 ID② 查余额(新玩家第一次查会自动领取初始道具):
curl -s "$API/v1/balance?playerId=$P" -H "Authorization: Bearer $PK"
# {"playerId":"qa-test-...","balance":{"h":3,"s":3,"m":3}}③ 发 5 个锤子:
curl -s -X POST $API/v1/grant -H "Authorization: Bearer $SK" -H 'Content-Type: application/json' \
-d "{\"playerId\":\"$P\",\"item\":\"h\",\"qty\":5,\"grantId\":\"order-$P-1\"}"④ 打开游戏查看(游戏会通过 /v1/tenant-config 自动获取 qa 的 pk,不需要再带 &pk=):
echo "https://xxl-classic-test.htdtek.com/?tenant=qa&player=$P"游戏每 3 秒刷新一次余额,锤子数应显示为 8。
2. 核心概念
租户(tenant)
每个买家是一个租户,用一个 ID 标识(小写字母、数字和 -,2–40 位,如 qa)。玩家余额、发放记录、图片包都按租户隔离:同一个 playerId 在不同租户下是两个互不相干的账户。
两种 Key
secret key sk_<租户>_… | publishable key pk_<租户>_… | |
|---|---|---|
| 放在哪 | 只能放在你的服务端 | 可以放在游戏前端 / App |
发道具 POST /v1/grant | ✅ | ❌ 403 |
查余额 GET /v1/balance | ✅ | ✅ |
扣减 POST /v1/consume | ✅ | ✅ |
通关奖励 POST /v1/reward | ✅ | ✅ |
Key 决定租户:服务端根据 Key 找到所属租户,请求里带 tenant 参数不会改变租户。Key 明文只在生成时显示一次,服务端只保存 SHA-256 哈希。
playerId 由谁传
playerId 是你(买家)自己系统里的玩家 ID,引擎不负责注册和登录。你的服务端发道具时传它,打开游戏时通过 URL 参数 player 传同一个值。格式:1–128 位,只能包含字母、数字、_ . : @ -(正则 ^[\w.:@-]{1,128}$)。
不传 player 时,游戏会自动生成一个 anon-<UUID> 的匿名 ID 并存在浏览器 localStorage 里,换设备或清缓存就会变。
3. 鉴权
所有 /v1/grant、/v1/balance、/v1/consume、/v1/reward 请求都要带 HTTP 头:
Authorization: Bearer sk_xxx- 没有这个头、格式不对、Key 不存在或已吊销:
401 {"error":"unauthorized"}。吊销立即生效。 - publishable key 调发放接口:
403 {"error":"forbidden","detail":"publishable key cannot grant"}。 GET /v1/images、GET /v1/tenant-config和/img/…是公开接口,不需要 Key。
4. 接口参考
基址 https://xxl-api-test.htdtek.com。请求体都是 JSON(Content-Type: application/json),响应都是 JSON。
POST/v1/grant · 发放道具(仅 secret key)
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| playerId | string | 是 | 玩家 ID,见上方格式 |
| item | string | 是 | h / s / m |
| qty | 整数 | 是 | 1–100000 |
| grantId | string | 是 | 你这边的唯一单号(如订单号),1–128 字符,用于幂等 |
curl -s -X POST https://xxl-api-test.htdtek.com/v1/grant \
-H "Authorization: Bearer sk_xxx" -H 'Content-Type: application/json' \
-d '{"playerId":"p1","item":"h","qty":5,"grantId":"order-1001"}'首次发放返回 201:
{"ok":true,"duplicate":false,
"grant":{"grantId":"order-1001","playerId":"p1","item":"h","qty":5,"created":1761400000000},
"balance":{"h":5,"s":0,"m":0}}"duplicate":true,grant 是第一次的原始记录,余额不变。网络超时可以放心用同一个 grantId 重试。同一个 grantId 但内容不同(2026-10-25 起):返回 409
{"error":"conflict","detail":"grantId already used with different playerId/item/qty","grant":{…原记录…}},不会发放。每笔新订单必须用新的 grantId。不同租户的 grantId 互不影响。created 是毫秒时间戳。保留前缀:grantId 不能以
init: 或 reward: 开头(系统内部用于初始道具和通关奖励),否则返回 400 {"error":"bad_request","need":"grantId must not start with reserved prefix init: or reward:"}。初始道具:对新玩家调用 grant 时,会先发放租户配置的初始道具,再加上本次数量(例如初始 3/3/3 的新玩家发 1 个锤子后余额为 h=4)。
错误:
| 状态码 | 响应 | 原因 |
|---|---|---|
| 400 | {"error":"bad_json"} | 请求体不是合法 JSON |
| 400 | {"error":"bad_request","need":"playerId,item(h|s|m),qty(1..100000 int),grantId"} | 字段缺失或不合法 |
| 401 | {"error":"unauthorized"} | Key 缺失 / 无效 / 已吊销 |
| 403 | {"error":"forbidden","detail":"publishable key cannot grant"} | 用了 publishable key |
| 409 | {"error":"conflict",…} | grantId 已用过且内容不同 |
GET/v1/balance?playerId= · 查询余额
curl -s "https://xxl-api-test.htdtek.com/v1/balance?playerId=p1" -H "Authorization: Bearer pk_xxx"{"playerId":"p1","balance":{"h":5,"s":0,"m":0}}没有记录的道具返回 0。响应头带 Cache-Control: no-store。错误:400 {"error":"bad_request","need":"playerId"}、401。
init:<playerId> 的发放记录),并发查询也只发一次。之后再查不会重复发。init: 是保留前缀,自己用会返回 400。修改配置只影响之后首次查询的玩家;在“初始道具”功能上线前就存在的玩家,下次查询时也会领一次。第一次调 grant(或 consume 之前的 balance)同样会触发初始道具。POST/v1/consume · 扣减道具
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| playerId | string | 是 | |
| item | string | 是 | h / s / m |
| qty | 整数 | 否 | 默认 1,范围 1–100000 |
| consumeId | string | 否(强烈建议) | 本次扣减的唯一单号,1–128 位,只能含 A-Z a-z 0-9 _ . : -。用于幂等 |
curl -s -X POST https://xxl-api-test.htdtek.com/v1/consume \
-H "Authorization: Bearer pk_xxx" -H 'Content-Type: application/json' \
-d '{"playerId":"p1","item":"h","qty":1,"consumeId":"c-123"}'
# 第一次 200 {"ok":true,"duplicate":false,"consumeId":"c-123","balance":{"h":2,"s":3,"m":3}}
# 同 id 再调 200 {"ok":true,"duplicate":true,"consumeId":"c-123","balance":{"h":2,"s":3,"m":3}} ← 不重复扣duplicate:true 和当前余额。同一个 consumeId 但 playerId/item/qty 不同,返回 409 {"error":"conflict","detail":"consumeId already used with different playerId/item/qty"}。不传 consumeId 仍可用(旧写法),响应只有
{"ok":true,"balance":{…}},但重试会重复扣。余额不足返回 409:
{"error":"insufficient","balance":{"h":0,"s":0,"m":0}},余额不会变成负数,也不会部分扣减。带 consumeId 时余额不足不会占用这个 id,充值后可用同一个 id 重试。其他错误:400 {"error":"bad_request","need":"playerId,item(h|s|m),qty(1..100000 int, default 1),consumeId(optional, 1..128 [A-Za-z0-9_.:-])"}、400 bad_json、401。
限流(2026-10-25 第二批起)
| 接口 | 默认阈值 | 计数维度 |
|---|---|---|
| POST /v1/grant | 600 次/分钟 | 每租户 |
| POST /v1/consume + POST /v1/reward(合计) | 120 次/分钟 | 每租户 + 每玩家 |
| GET /v1/balance、/v1/tenant-config、/v1/images | 不限流 | —(游戏 3 秒轮询不受影响) |
按固定 60 秒窗口计数。每个租户可在后台单独设置,填 0 表示不限。超限返回 429,并带 Retry-After 响应头(秒):
HTTP/1.1 429 Too Many Requests
Retry-After: 37
{"error":"rate_limited","detail":"…","retryAfter":37}retryAfter(或 Retry-After 头)等待后再发。consume 重试时继续用同一个 consumeId,不会重复扣。界面提示「操作太频繁,请稍后」并保持按钮锁定到等待结束。服务端发放(grant)请排队或退避,不要并发猛刷;grantId 幂等,重试安全。推荐的客户端重试方式
- 玩家点道具时先生成一个 consumeId(如
playerId-时间戳-随机数),并锁住道具按钮,防止连点。 - 每次请求最多等 8 秒,超时或网络错误时用同一个 consumeId 重试,最多重试 2 次。
- 拿到 200(无论 duplicate 是 true 还是 false)才让道具生效;409 insufficient 提示余额不足;429 按 retryAfter 等待后用同一个 consumeId 再试;全部失败提示「未扣除」并解锁按钮。
- 超过约 1.2 秒可以先提示「网络较慢」。
async function consume(api, pk, playerId, item) {
const consumeId = `${playerId}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
for (let i = 0; i < 3; i++) { // 1 次 + 重试 2 次,都用同一个 consumeId
const ac = new AbortController(), t = setTimeout(() => ac.abort(), 8000);
try {
const r = await fetch(api + '/v1/consume', { method: 'POST', signal: ac.signal,
headers: { Authorization: 'Bearer ' + pk, 'Content-Type': 'application/json' },
body: JSON.stringify({ playerId, item, qty: 1, consumeId }) });
if (r.status === 429) { const j = await r.json(); return [429, j]; } // 调用方按 j.retryAfter 等待后用同一 consumeId 重试
if (r.status === 200 || r.status === 409 || r.status === 400) return [r.status, await r.json()];
} catch (e) { /* 超时或断网,继续重试 */ } finally { clearTimeout(t); }
}
return [0, { error: 'not_charged' }]; // 未扣除
}POST/v1/reward · 通关奖励(pk 或 sk)
游戏通关时调用。同一租户、同一玩家、同一关只发一次。奖励内容和开关由租户在后台配置,默认锤子×1。内部记为 grantId=reward:<关卡>:<playerId>。
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| playerId | string | 是 | |
| level | 整数 | 是 | 1–1000 |
curl -s -X POST https://xxl-api-test.htdtek.com/v1/reward \
-H "Authorization: Bearer pk_xxx" -H 'Content-Type: application/json' \
-d '{"playerId":"p1","level":1}'
# 首次 201 {"ok":true,"granted":true,"level":1,"reward":{"item":"h","qty":1},"balance":{"h":5,"s":2,"m":2}}
# 再调 200 {"ok":true,"granted":false,"level":1,"reward":{"item":"h","qty":1},"balance":{…}}
# 后台关闭时 200 {"ok":true,"granted":false,"reason":"disabled","balance":{…}}错误:400 {"error":"bad_request","need":"playerId,level(1..1000 int)"}、400 bad_json、401。
/v1/grant 发放。GET/v1/tenant-config?tenant= · 租户配置(公开)
不需要 Key。游戏启动时调用,拿到该租户的游戏 pk 和道具配置。
curl -s "https://xxl-api-test.htdtek.com/v1/tenant-config?tenant=qa"
# 200 {"tenant":"qa","name":"QA测试","publishableKey":"pk_xxx",
# "initItems":{"h":3,"s":3,"m":3},"levelReward":{"item":"h","qty":1},
# "endgameMode":"ch1","imagePack":"male"}publishableKey:后台设置的游戏 pk,未设置时为null(游戏道具不可用)。levelReward:通关奖励关闭时为null。endgameMode(第三批新增):残局规则,取值ch1(默认:第 1 章自动翻开,第 2 章起弹窗)/auto(所有章节自动翻开)/popup(所有章节弹窗)/off(都不启用)。见下方「残局规则」。imagePack(第三批新增):当前启用的图片包名,如male、custom;default表示使用内置默认插画;老租户从未设置过时可能为null。- 不存在的租户或 tenant 为空:404
{"error":"not_found","detail":"unknown tenant"}。
GET/v1/images?tenant= · 图片包列表(公开)
curl -s "https://xxl-api-test.htdtek.com/v1/images?tenant=qa"
# {"tenant":"qa","pack":"male","images":[{"level":1,"order":1,"url":"https://xxl-api-test.htdtek.com/img/qa/1-ab12cd34.webp"}]}pack(第三批新增):当前启用的图片包名。default 时 images 为空,游戏使用内置默认插画;有图但从未建过包的老租户显示 legacy;images 只返回启用包里的图。
第四批起:不存在的租户返回 404 {"error":"unknown_tenant","detail":"tenant not found","tenant":"<传入的 tenant>"};已存在的租户(包括还没有图的)仍返回 200。
按关卡升序。可缓存 30 秒。图片本身 GET /img/<路径>,不存在返回 404 纯文本 not found。
残局规则(第三批起)
步数用完、图片还没全部揭开时,游戏按 endgameMode 处理:
- 自动翻开:如果只剩不超过 3 个普通格,直接翻开并闪光提示,判为通关。冰块、石块、锁链格不会自动翻开。
- 弹窗:弹出「只差 N 格!+5 步继续?」。点「使用」扣 1 个 +5 步道具(走
/v1/consume,带 consumeId),步数变成 5 继续游戏;道具数为 0 时「使用」按钮置灰并提示;点「放弃」判负。+5 步用完后又没步数时会再次弹出,每次使用扣 1 个。 - 默认
ch1:第 1 章(新手)自动翻开,第 2 章起弹窗。
通用状态码
| 码 | 含义 |
|---|---|
| 200 | 成功 / 重复请求 |
| 201 | 新发放 / 通关奖励发放成功 |
| 204 | CORS 预检 OPTIONS |
| 400 | 参数或 JSON 错误 |
| 401 | Key 无效(/v1/ 下未知路径,不带 Key 时也先返回 401) |
| 403 | 无权限 |
| 404 | {"error":"not_found"} 路径或方法不存在 |
| 409 | 余额不足(insufficient)/ grantId 或 consumeId 内容冲突(conflict) |
| 429 | {"error":"rate_limited","detail","retryAfter"} + Retry-After 头,见上文限流;后台登录被锁定也返回 429 |
| 500 | {"error":"server_error"} |
5. 道具类型
| 代码 | 道具 | 游戏内效果 |
|---|---|---|
h | 🔨 锤子 | 点一格直接敲掉 |
s | 🔀 洗牌 | 重新打乱棋盘 |
m | ➕ 加步 | 本关 +5 步 |
目前只支持这三种,传其他值返回 400。
6. 把游戏嵌进你的 App 或网页
URL 参数
| 参数 | 说明 |
|---|---|
tenant | 租户 ID,决定加载哪个图片包。默认 demo |
player | 玩家 ID,和你发道具时用的 playerId 一致。不传则生成匿名 ID |
pk | 可选。游戏会通过 /v1/tenant-config 自动获取该租户在后台设置的 pk,一般不用传;传了则覆盖自动获取的值。拿不到配置时道具按钮置灰、数量显示「–」并提示「未知租户」或「连接失败」 |
api | API 地址,一般不用改 |
https://xxl-classic-test.htdtek.com/?tenant=qa&player=user_123iframe 嵌入网页
<iframe src="https://xxl-classic-test.htdtek.com/?tenant=qa&player=user_123"
style="width:100%;height:100vh;border:0" allow="autoplay; fullscreen"></iframe>App 里用 WebView(iOS WKWebView / Android WebView)打开同一个 URL 即可,需开启 JavaScript 和 localStorage。声音需要用户点一下「点击开始」才会解锁(浏览器限制)。
道具同步方式
游戏每 3 秒(页面可见时)拉一次 /v1/balance,切回页面时立即拉一次;玩家使用道具时调用 /v1/consume 扣 1 个(带 consumeId,8 秒超时,同 id 重试 2 次,等待期间按钮锁住);通关时调用 /v1/reward 领通关奖励。你服务端发放后,游戏里最多约 3 秒显示新数量。
CORS 白名单
浏览器直接调 API 时,只有白名单里的域名能读到响应。当前白名单:xxl-test、xxl-mosaic-test、xxl-classic-test(均为 .htdtek.com)及本地 localhost/127.0.0.1:8080、8091。用 iframe 嵌我们的游戏不需要申请。如果你要在自己的网页前端直接调 API,请按 第 11 节把完整 Origin(如 https://game.example.com)发给管理员添加。服务端调 API 不受 CORS 限制。允许的方法 GET/POST/OPTIONS,允许的头 Authorization、Content-Type。
7. 管理后台
地址 https://xxl-admin-test.htdtek.com,用管理员密码登录(会话 12 小时)。测试团队如需后台操作请联系管理员。
- 创建租户:首页填 tenant id(小写字母数字和 -,2–40 位)和名称 → 创建。已存在的 id 不会覆盖。
- 生成 Key:进入租户 → API Keys → 选 secret 或 publishable → 生成。明文只显示这一次,列表里只显示前缀。
- 吊销 Key:在列表里点「吊销」,立即生效,之后用它请求返回 401。不能恢复,只能重新生成。
- 初始道具:「新玩家初始道具」填 h / s / m 数量(0–999)→ 保存。
- 游戏 pk:「游戏前端 publishable key」填一个本租户未吊销的 pk → 保存,游戏通过
/v1/tenant-config公开下发。新建 publishable key 时如果还没设置会自动填上。未设置时游戏道具不可用。 - 通关奖励:勾选开关、选道具(h/s/m)和数量(0–99)→ 保存。默认开启、锤子×1。关闭后
/v1/reward返回granted:false, reason:"disabled"。 - 手动发道具:填 playerId、选道具和数量 → 发放(grantId 自动生成为
admin-…)。 - 上传图片包:填「从第 N 关开始」,一次选多张图,按选择顺序依次对应第 N、N+1…关。浏览器会自动居中裁成 1:1(最大 1024px)并压成 WebP;单张处理后需 ≤3MB,支持 JPG/PNG/WebP。同一关重复上传会替换。可逐张删除。
服务端校验(第四批):按文件头(magic bytes)判断真实类型,只接受 JPEG/PNG/WebP,扩展名或声明类型不可信,不符返回 415;单个文件为 0 字节或超过 3,000,000 字节返回 413;关卡号不在 1–50,或从第 N 关起的张数超出第 50 关,返回 400;多张一起上传时先全部校验,任一张不合格整批拒绝、一张都不写入;存储写入失败时回滚已上传的文件并返回 500。替换或删除图片后,不再被任何图片包引用的旧文件会自动删除,旧图片地址随后返回 404。 - 图片包切换(第三批):同一个租户可以存多组图片包。租户页 →「图片包切换」列出所有包和图片数,点「启用」一键切换(几秒内生效,
/v1/images可缓存 30 秒),点「恢复默认」切回内置默认插画。「新建空包并启用」可新建一个包(包名为字母数字 _ -,1–32 位),之后上传的图进入该包。上传和删除只作用于当前启用的包。对还没有图片包(处于 default)的租户上传图片时,会自动新建一个custom包并启用。接口:POST /t/<id>/pack(表单name,可选act=new;不存在的包 404,非法包名 400,包已存在 409)。 - 残局规则(第三批):租户页 →「残局规则」选择 ch1 / auto / popup / off → 保存,通过
/v1/tenant-config的endgameMode下发。接口:POST /t/<id>/endgame(表单mode,非法值 400)。 - 限流设置:租户页 →「限流」,分别填 grant(每租户/分钟)和 consume+reward(每玩家/分钟),0 = 不限 → 保存。
- 最近发放:显示该租户最近 20 条发放记录(含初始道具和后台手动发放)。
8. 端到端测试流程
export API=https://xxl-api-test.htdtek.com SK=sk_xxx PK=pk_xxx
export P=qa-e2e-$(date +%s)
# 1. 首次查询:应为 3/3/3(初始道具)
curl -s "$API/v1/balance?playerId=$P" -H "Authorization: Bearer $PK"
# 2. 手机或电脑打开游戏,进第 2 关,锤子应显示 3
echo "https://xxl-classic-test.htdtek.com/?tenant=qa&player=$P"
# 3. 发 2 个锤子 → 201,h=5;3 秒内游戏里锤子变 5
curl -s -w ' [%{http_code}]\n' -X POST $API/v1/grant -H "Authorization: Bearer $SK" \
-H 'Content-Type: application/json' -d "{\"playerId\":\"$P\",\"item\":\"h\",\"qty\":2,\"grantId\":\"e2e-$P\"}"
# 4. 同一个 grantId 再发 → 200 duplicate:true,h 仍是 5
# 5. 在游戏里用一次锤子(点锤子再点棋盘一格)→ 游戏显示 4
# 6. 再查余额 → h=4
curl -s "$API/v1/balance?playerId=$P" -H "Authorization: Bearer $PK"9. 测试用例清单
| # | 用例 | 期望 |
|---|---|---|
| 1 | 新玩家首次查余额 | 200,h/s/m = 3/3/3 |
| 2 | 同一玩家再次查余额 / 5 个并发首次查询 | 仍是 3/3/3,不重复发 |
| 3 | sk 发放新 grantId | 201,duplicate:false |
| 4 | 同一 grantId、相同内容重发 | 200,duplicate:true,余额不变 |
| 4a | 同一 grantId 改 qty 或 item 重发 | 409 conflict,余额不变 |
| 4b | grantId 以 init: 或 reward: 开头 | 400 |
| 5 | pk 扣减 qty=1 / 不传 qty | 200,余额 -1 |
| 5a | 同一 consumeId 调两次 / 并发 5 次 | 只扣一次,第二次起 duplicate:true |
| 5b | 同一 consumeId 改 item 或 qty | 409 conflict |
| 5c | /v1/reward 同一玩家同一关调两次 | 第一次 201 granted:true,第二次 200 granted:false |
| 5d | tenant-config?tenant=qa / 不存在的租户 | 200 带 publishableKey / 404 not_found |
| 5e | 打开 ?tenant=qa 不带 pk;快速连点道具 | 道具正常同步;连点只扣 1 个 |
| 6 | 游戏内发放后 3 秒内显示、使用后服务端同步 | 通过 |
| 应该失败的情况 | ||
| 7 | 不带 Authorization | 401 unauthorized |
| 8 | 错误 Key / Bearer 后为空 / 已吊销 Key | 401 |
| 9 | pk 调 /v1/grant | 403 forbidden |
| 10 | 扣减超过余额 | 409 insufficient,余额不变 |
| 11 | item 为 x、qty 为 0 / -1 / 1.5 / "5" / 100001 | 400 bad_request |
| 12 | 缺 grantId 或 playerId;playerId 含空格、中文或超过 128 位 | 400 |
| 13 | 请求体不是 JSON | 400 bad_json |
| 14 | balance 不传 playerId | 400 need:playerId |
| 15 | 用 demo 的 Key 查/扣/发 qa 的玩家;请求里加 tenant=qa | 只作用在 demo 自己的空间,qa 玩家余额不变 |
| 16 | GET /v1/grant、POST /v1/balance、/v1/unknown(带 Key) | 404 not_found |
| 17 | 未登录访问后台 /t/qa、伪造 cookie | 401 登录页 |
| 18 | API 域名访问 /admin | 404 |
| 19 | 非白名单 Origin 的浏览器请求 | 响应无 Access-Control-Allow-Origin,浏览器拦截 |
| 20 | 同一玩家 1 分钟内并发 130 次 consume(默认阈值) | 前 120 次正常,之后 429 rate_limited + Retry-After;另一玩家同时正常 |
| 21 | 同一玩家连续 150 次 balance | 全部 200(不限流) |
| 22 | 1 分钟内 grant 超过 600 次(每租户) | 429;下一窗口恢复 201 |
| 23 | 收到 429 后等 retryAfter 秒,用同一 consumeId 重试 | 200,只扣一次 |
| 24 | 后台登录密码错 6 次 | 第 1–5 次 401,第 6 次 429;锁定期间正确密码也 429,API 照常 200 |
| 25 | 新玩家不查余额直接 consume | 先发初始道具再扣减,返回 200 |
| 26 | GET /v1/tenant-config?tenant=qa | 200,含 endgameMode(默认 ch1)和 imagePack |
| 27 | GET /v1/images?tenant=qa | 200,含 pack,images 只含启用包的图 |
| 28 | 后台切到 default,再查 /v1/images | pack=default,images 为空,游戏显示内置插画 |
| 29 | 后台切回原包(如 male) | 图片完整恢复,张数不变 |
| 30 | 启用不存在的包 / 非法包名 | 404 / 400 |
| 31 | 对处于 default 的租户上传图片 | 自动新建 custom 包并启用,图进入该包 |
| 32 | 第 1 章步数用完、剩 ≤3 个普通格(endgameMode=ch1) | 自动翻开并通关 |
| 33 | 第 2 章起步数用完未揭完 | 弹「只差 N 格!+5 步继续?」;使用→步数 5、扣 1 个;道具 0 时按钮置灰;放弃→判负 |
| 34 | POST /t/<id>/endgame mode=popup,再查 tenant-config | endgameMode=popup;非法 mode 返回 400 |
| 35 | GET /v1/images?tenant=不存在的租户 | 404 unknown_tenant,带 detail 和 tenant |
| 36 | 后台上传改了扩展名的非图片文件(如 .txt 改成 .jpg) | 415,不写入 |
| 37 | 上传超过 3,000,000 字节的文件 / 0 字节文件 | 413 |
| 38 | 起始关卡填 0 或 51;从第 49 关起传 3 张 | 400 |
| 39 | 一次上传一张好图和一张坏图 | 整批拒绝(415),两张都不写入 |
| 40 | 替换或删除某关图片后访问旧图片地址 | 404 |
10. 已知限制
- 这是测试环境(域名带 -test),数据可能被清理,不保证 SLA。
- 后台只有一个总管理员密码,登录后能看到所有租户;分租户登录还没做。
- 图片只能自动居中裁切,还没做拖动选择裁切区域。
- 后台登录锁定按 IP 计算:攻击者能换不同 IP 段时拦不住;暂无全局锁(避免挡住正常管理员),分租户登录时一并解决。
- 没有 webhook 回调、没有发放记录查询 API。
- 道具只有 h/s/m 三种。
/v1/reward用公开 pk 调用,玩家最多能刷到「关卡数 × 奖励数量」(默认每关锤子×1)。- 还没在真 iPhone / Android 上做完整测试。
12. 更新记录
2026-10-26(第四批修复)
GET /v1/images对不存在的租户改为返回 404{"error":"unknown_tenant","detail","tenant"}(之前是 200 空列表)。已存在的租户不受影响。- 后台上传新增服务端校验:按文件头判断类型(415)、单文件 1–3,000,000 字节(413)、关卡范围 1–50(400)、任一张不合格整批拒绝、写入失败回滚(500)。
- 替换或删除图片后自动清理不再被引用的旧文件,旧地址返回 404。
- 其他字段和路径未改名、未删除。
2026-10-25(第三批修复,向后兼容)
GET /v1/images新增pack字段。GET /v1/tenant-config新增endgameMode(ch1 / auto / popup / off,默认 ch1)和imagePack。- 后台新增图片包切换(
POST /t/<id>/pack):每个租户可存多组图,一键切换或恢复默认;对无图片包的租户上传时自动新建并启用custom包。 - 后台新增残局规则设置(
POST /t/<id>/endgame)。 - 游戏:第 2 章起步数用完弹「只差 N 格!+5 步继续?」;横屏 844×390 布局修复;切到后台暂停音频;断网恢复后提示条立即清除;图鉴计数通关后立即刷新。难度曲线仍在调整中。原有字段和路径无改名或删除。
2026-10-25(第二批修复,向后兼容)
- 新增限流:grant 每租户 600 次/分钟;consume+reward 每玩家 120 次/分钟;balance、tenant-config、images 不限;固定 60 秒窗口;后台可按租户配置,0 = 不限。
- 新状态码 429
{"error":"rate_limited","detail","retryAfter"}+Retry-After头。 - 后台登录防暴力破解:按 IP 5 次失败锁 5 分钟,翻倍至最长 30 分钟;API Key 不受影响。
- 新玩家首次直接 consume 会先发初始道具(以前返回 409)。
- 游戏:第 1 关残局兜底,保证可通关。原有字段和路径无改名或删除。
2026-10-25(第一批修复,向后兼容)
- 新增
GET /v1/tenant-config:公开下发游戏 pk 和初始道具/通关奖励配置;游戏不再需要&pk=,?pk=仍可覆盖。 - 新增
POST /v1/reward:通关奖励,每租户+玩家+关卡只发一次,后台可配置,默认锤子×1。 /v1/consume新增可选consumeId(幂等),响应带duplicate、consumeId;400 错误带need;qty 上限 100000。- grantId / consumeId 重复但内容不同返回 409
conflict(以前 grant 返回 200)。 init:、reward:成为保留前缀,买家使用返回 400。- grant 也会先给新玩家发初始道具。
- 后台新增「游戏 pk」和「通关奖励」设置。
- 游戏:道具连点加锁、8 秒超时同 id 重试、断网/未知租户时按钮置灰并提示。
11. 问题反馈
请发给 Tan Wei(项目负责人),每个问题注明:
- 时间(注明时区,如 UTC+8)、租户、playerId、grantId
- 完整请求(把 Key 换成 sk_xxx / pk_xxx)、状态码和响应体
- 游戏问题附设备、浏览器和截图或录屏
申请 CORS 白名单、新的测试 Key 或吊销 Key,也找同一个人。