# ZYMIX 小游戏通用后台接口文档

## 服务入口

当前服务入口 `https://testgame.horkmail.com`。

| 路径 | 内容 |
|---|---|
| /admin | 管理后台，账号密码登录 |
| /docs | 本接口文档网页 |
| /docs/api.md | Markdown 源文件 |
| /docs/developer | 原钉钉开发者文档整理网页 |
| /docs/developer.md | 钉钉文档 Markdown |
| /health | 存活探针，包含 revision |
| /ready | MySQL 和 Redis 就绪检查，失败 503 |

API 使用 JSON，成功包含 `ok: true`，错误包含 `ok: false, code, message`。业务写请求在 MySQL 事务提交后响应。请求体最大 1 MiB，必须为 JSON 对象。时间为 ISO 8601 UTC。客户端收到 429/503 应退避重试；一次性登录 code 不得重复兑换。

## 管理端登录

管理 API 前缀 `/api/admin`，浏览器同源使用 Cookie，不支持手工 ADMIN_TOKEN。所有非 GET 操作均需 `X-Requested-With: ZymixAdmin`。浏览器跨站 Origin 被拒绝。

```javascript
await fetch('/api/admin/auth/login', {
  method: 'POST', credentials: 'same-origin',
  headers: {'Content-Type':'application/json', 'X-Requested-With':'ZymixAdmin'},
  body: JSON.stringify({username:'zymix', password:'管理员密码'})
});
```

| 方法和路径 | 请求 | 成功响应 |
|---|---|---|
| POST /api/admin/auth/login | username、password | username、expiresAt、renewAfterSeconds，Set-Cookie |
| GET /api/admin/auth/me | Cookie | username、expiresAt、renewAfterSeconds |
| POST /api/admin/auth/refresh | Cookie、防跨站请求头 | 会话信息，到轮换时间时 Set-Cookie |
| POST /api/admin/auth/logout | Cookie、防跨站请求头 | ok，删除服务端会话并清除 Cookie |

Cookie 为 HttpOnly、SameSite=Strict；HTTPS 使用 Secure。局域网 HTTP 显式 COOKIE_SECURE=false。默认空闲 1 小时、最长 12 小时、每 10 分钟轮换；旧令牌兼容 30 秒以支持并发标签页。页面自动续期，401 时要求重新登录。Redis 故障不放行鉴权。

## 项目配置

| 方法和路径 | 说明 |
|---|---|
| GET /api/admin/projects | 返回 projects，最多 1000 个，按创建时间降序 |
| POST /api/admin/projects | 创建，成功 201，返回 project，version=1 |
| GET /api/admin/projects/:id | 获取完整脱敏配置 |
| PATCH /api/admin/projects/:id | 修改，必须携带读取时的 version，成功 version+1 |
| DELETE /api/admin/projects/:id | 删除项目及其会话、存档、事件、领奖、防重记录 |
| GET /api/admin/projects/:id/stats | 统计，支持 from/to ISO 日期 |

AppID 必填且唯一，1～128 个字母、数字、下划线、连字符，创建后不可变更。name 必填，最长 200 字符。AppSecret 加密保存，管理回显 `[redacted]`；未配置时为空。空值、缺省、`[redacted]` 保留已有密钥，`clearAppSecret:true` 显式清除。

TCMPP 最小创建示例：

```json
{
  "name": "示例小游戏",
  "appId": "your-game-appid",
  "appSecret": "从控制台取得的密钥",
  "enabled": true,
  "environment": "production",
  "auth": {
    "protocol": "tcmpp",
    "jscode2sessionUrl": "https://实际OpenServer域名/openserver/sns/jscode2session",
    "requestTimeoutMs": 5000,
    "providers": {}
  }
}
```

| 配置 | 说明 |
|---|---|
| auth.protocol | 默认 tcmpp；明确选择时支持 wechat-mini-game / qq-mini-game |
| auth.jscode2sessionUrl | TCMPP 控制台完整 URL，保留路径，不使用微信默认地址 |
| auth.requestTimeoutMs | 200～10000 毫秒 |
| auth.providers | 按 platform 映射独立 appId、appSecret、protocol、jscode2sessionUrl、requestTimeoutMs |
| domains | request/upload/download/socket/business 数组，openServer 展示配置；真正兑换使用 auth 的 URL |
| share | enabled、shareKeys、idRange、channels |
| ads.rewardedVideo | 广告配置数组，placement、adUnitId、adUnitIds、platforms、enabled、reward、最低宿主版本 |
| reward | type 为 1～40 位字母数字下划线连字符，amount 为 0～1000000 整数 |
| platforms | wx/tt/ks/my/bl 的分享模板、订阅模板 ID、feed 内容 ID 配置 |
| gray | enabled、rules，按 priority 降序匹配白名单或百分比 |
| minHostVersion | android / ios 最低版本配置 |
| privacyPolicyUrl | 隐私协议地址 |
| publicMetadata | 公开扩展配置，禁止放入任何敏感数据 |
| internalMetadata | 只在后台返回的内部扩展配置 |

共享根凭据和独立 provider 的空白密钥保存规则一致。删除 provider 配置时提交更新后的 providers 全量对象。OpenServer 主机必须由服务器运维命令 `sh deploy/ops.sh allow-host 域名` 加入 MySQL 安全配置允许列表，网页和 HTTP API 不提供该列表的修改接口，只允许公网 HTTPS，不跟随重定向。

统计返回：`stats.totalEvents`、`stats.uniqueUsers`、`stats.byType`、最近 30 条 `stats.recentEvents`。GET 配置不写事件；需要统计曝光时客户端显式上报事件。

## 小游戏登录

`POST /api/projects/:appId/login`，无需业务 token。

```json
{"code":"wx.login 返回的新 code", "platform":"wx", "userInfo":{"nickname":"玩家"}}
```

platform 默认为 wx。wx/tcmpp 默认使用根凭据，其他标识必须在 auth.providers 显式配置。本接口只支持已实现的 TCMPP、微信、QQ 换码协议；配置 tt/ks 等标识不意味着实现了其原生专有登录协议。

后台 GET 控制台提供的完整 URL，参数为 `appid`、`secret`、`js_code`、`grant_type=authorization_code`。TCMPP 详细文档路径包含 `/openserver/sns/jscode2session`；教程示例不同，因此以控制台为准。不能用 console.tcmpp.com 登录页面代替。

```json
{
  "ok": true,
  "token": "业务签名token",
  "expiresAt": "2026-09-17T00:00:00.000Z",
  "user": {"userId":"usr_项目隔离标识", "userInfo":{"nickname":"玩家"}}
}
```

不返回 AppSecret、openid、session_key。客户端提交 userId 不改变身份。userInfo 是客户端资料，不能当作平台认证的权限或身份声明。

后续业务接口携带 `Authorization: Bearer <token>`。默认有效期 7 天，由 SESSION_TTL_SECONDS 配置；到期或 Redis 会话丢失后重新 wx.login。管理员 Cookie 续期与小游戏 token、腾讯 getAccessToken 是不同机制。

`GET /api/session/me` 返回 session 的 id、appId、userId、userInfo、createdAt、expiresAt。

## 公开配置和灰度

| 方法和路径 | 请求 | 响应 |
|---|---|---|
| GET /api/projects/:appId/config | 无 | config，安全白名单字段 |
| GET /api/projects/:appId/share-options | ?id=101&shareKey=zymixFriends | share、validation；不合法 400 |
| GET /api/projects/:appId/ads/rewarded | ?placement=default，all 返回所有 | ads，已启用广告 |
| POST /api/projects/:appId/gray/evaluate | userId/deviceId/guid | gray.matched、rule、version |

灰度匹配是客户端发布选择提示，不能用作访问敏感资源的权限控制。公开配置不包含内部元数据、认证 provider 或密钥。

## 存档

客户端请优先阅读 [小游戏云存档接入标准](/docs/save)，包含完整请求响应、version 管理、428/409 处理和 wx.request 示例；[Markdown 源文件](/docs/save.md)。

`GET /api/projects/:appId/data/:key` 要求业务 token，按项目和用户隔离。

```json
{"ok":true,"key":"save","exists":false,"value":null,"version":0,"updatedAt":null}
```

`PUT /api/projects/:appId/data/:key`：

```json
{"version":0,"value":{"level":1,"coins":0}}
```

创建时 version=0；更新时传最近读到的 version。成功返回 key、value、新 version。并发修改只有一个版本成功，其余 409，客户端应重新读取并合并业务状态，不能无条件覆盖。缺版本 428。key 最长 128 字符。存档属于客户端数据，不能作为可信积分余额。

## 事件和分享回调

`POST /api/projects/:appId/events` 要求业务 token：

```json
{"type":"level_complete","payload":{"level":1}}
```

持久化后返回 202。type 为字母开头、最长 80 位的字母数字下划线点连字符；禁止伪造 login/reward_granted/project_created/project_updated 系统事件。payload 敏感键会脱敏。

`POST /api/projects/:appId/share/callback` 要求业务 token，提交 id、shareKey 及业务信息；写入分享校验事件，合法 200，不合法 400。

## 激励奖励确认

钉钉文档要求前端仅在广告 onClose 且完整观看时发放本地奖励。客户端 isEnded 不能证明服务端奖励可信。本后台持久化奖励增加可信业务服务 HMAC 确认，未配置确认服务时返回 503，不能仅凭 isEnded=true 入账。

`POST /api/projects/:appId/ads/reward/complete` 要求业务 token：

```json
{
  "traceId":"可信服务签发的一次性业务追踪号",
  "adUnitId":"已配置广告位",
  "isEnded":true,
  "expires":1789023000,
  "proof":"可信服务签名"
}
```

可信服务在核验平台回调或业务依据后计算 HMAC-SHA256，使用独立 REWARD_PROOF_SECRET，对 `JSON.stringify([projectId,userId,adUnitId,traceId,expires])` 签名，输出 base64url。expires 为未来最多 600 秒的 Unix 秒。签名密钥只能在可信服务器，不能放入小游戏、管理页面或公开配置。代码参考 `src/reward-proof.js`。本服务不声称验证了腾讯广告平台回调；没有可信确认链路时只使用前端本地奖励或暂停服务端发奖。

成功返回 duplicate 与 reward（traceId、userId、adUnitId、reward、grantedAt）。同项目同 traceId 持久化一次，并发不会重复记账。签名错误 403；traceId 已归属另一用户 409。这是发奖记录，不是资金账户或积分钱包。

## 错误码和客户端处理

| HTTP | 常见 code | 处理 |
|---|---|---|
| 400 | invalid_project_identity / invalid_provider_url / invalid_event_type / invalid_reward | 修正参数 |
| 400 | code_required / invalid_platform / data_key_invalid / data_value_required | 补齐或修正参数 |
| 401 | admin_login_required / invalid_credentials / session_expired / invalid_login_code | 重新登录；小游戏重新获取 code |
| 403 | csrf_rejected / invalid_reward_proof / reward_proof_expired | 核对同源请求头或可信确认签名 |
| 404 | project_not_found / ad_not_found / not_found | 检查项目、广告和路径 |
| 409 | conflict / project_version_conflict / data_version_conflict | 唯一键或版本竞争；重新读取 |
| 409 | code_consumed / reward_owner_mismatch | 获取新 code；拒绝跨用户奖励重用 |
| 413 | request_error | 请求体过大 |
| 428 | project_version_required / data_version_required | 带上读取时版本 |
| 429 | rate_limited / login_busy | 退避，避免立即无限重试 |
| 502 | code_exchange_failed / code_exchange_http_error / code_exchange_network_error / code_exchange_invalid_response | 检查平台服务、凭据和配置；原始响应不会透出 |
| 503 | database_unavailable / service_unavailable | 存储不可用，退避重试 |
| 503 | app_secret_not_configured / code_exchange_not_configured / reward_confirmation_not_configured | 管理员补全服务端配置 |
| 504 | code_exchange_timeout | 获取新 code 后重试 |

平台 code 兑换失败可能附带数字 `details.providerCode`，不回显上游 errmsg 或 URL 查询参数。客户端网络超时可能发生在数据库已提交之后：存档需读取最新版本，奖励用相同追踪号检查，不能生成新编号重复发奖。
