# 用户认证接口

---

## Member 对象 {#member}

字段 | 类型 | 说明
---|---|---
id | string | 会员 ID（Snowflake）
username | string | 用户名（注册时提交的账号原值：手机号或邮箱）；游客为客户端随机生成（`g` 开头 + 字母数字，见【[游客注册](#guest_register)】）。用户名一经创建不可修改
nickname | string | 昵称
head_portrait | string | 头像 URL；注册时若商户配置了预设头像则随机分配一张（见【[可选头像列表](500_site_content.md#avatars)】），否则为空串
merchant_id | string | 所属商户 ID
register_type | string | 注册方式，`phone` 手机号 / `email` 邮箱 / `guest` 游客（见【[游客注册](#guest_register)】）；存量会员为 `phone`。**这是注册来源，【[绑定账号](#bind_account)】转正后不变**；判断当前是否游客请看 `is_guest`
is_guest | bool | 是否游客。【[游客注册](#guest_register)】创建的账号为 `true`；【[绑定账号](#bind_account)】并设置密码转正后变为 `false`。客户端应据此展示「绑定账号 / 设置密码」引导，而非 `register_type`
phone | string | 登录手机号（纯号码，**不含区号**）。手机号注册时写入；存量会员与未配置区号商户的部分会员可能为空串
area_code | string | 国际手机区号（纯数字，如 `55`），未配置时为空串
email | string | 邮箱（服务端已归一化为小写）。邮箱注册时即登录邮箱；手机号注册的会员该字段也可能是 [PIX 收款资料](200_finance.md#pix_create)填写的收款邮箱——两种来源**共用同一字段**，同商户内唯一
phone_verified | bool | 手机号是否已通过真实性验证。**当前注册流程不做任何验证，恒为 `false`**；该字段是预留给提款等敏感操作的风控门槛，**与能否登录无关**
email_verified | bool | 邮箱是否已通过真实性验证。**当前注册流程不做任何验证，恒为 `false`**；用途同 `phone_verified`，**与能否登录无关**
channel_id | string | 渠道 ID
source | string | 来源标记
inviter_id | string | 直接上级会员 ID（无上级时为 `"0"`）
promoter_code | string | 自己的推广码
current_level | number | 当前等级
is_ad_reported | bool | 是否已上报广告归因
reported_at | string | 广告归因上报时间
auto_withdraw | number | 自动提现规则，`0` 默认 / `1` 自动 / `2` 禁止
register_ip | string | 注册 IP
is_active | bool | 是否启用
last_login | string | 上次登录时间
date_joined | string | 加入时间
created_at | string | 创建时间
updated_at | string | 更新时间

> 游客的账号密码由客户端在【[游客注册](#guest_register)】时自行随机生成并持久化，服务端**不会**在任何接口返回密码（含本对象）。

---

## 注册 {#register}

注册成功后自动登录并返回 token。

**URL:** `POST` /auth/register

**输入:**

字段 | 类型 | 必填 | 说明
---|---|---|---
username | string | 是 | 账号，长度 2-64。`account_type=phone` 时传**手机号**（纯号码，不含区号）；`account_type=email` 时传**邮箱**
password | string | 是 | 密码，长度 4-64
account_type | string | 否 | 账号类型：`phone` 手机号 / `email` 邮箱。**留空按 `phone` 处理**，旧客户端不传时行为与之前完全一致；传枚举以外的值会被参数校验拦截（`400`），不会创建账号
nickname | string | 否 | 昵称，长度不超过 60
promoter_code | string | 否 | 邀请人的推广码，长度不超过 50
channel_id | string | 否 | 渠道 ID
source | string | 否 | 来源标记，pwa、h5 等；传 `pwa` 时服务端同时标记为 PWA 用户（浏览器注册后再安装 PWA 的场景见【[上报 PWA 安装态](110_pwa.md#activate)】）
link_queries | string | 否 | 落地页 URL 的**完整 query string**，原样传入（可带 `?` 前缀），长度不超过 1024。服务端会从中解析 `fbclid` 参数，构造 Facebook 点击 ID（`fb.1.{注册时刻毫秒}.{fbclid}`）存库，用于后续 Facebook 广告归因上报（FB CAPI）——投放场景务必透传，勿丢弃 `fbclid`。**APK / IPA WebView 场景**：原生包会在 URL 上追加 `app_id` / `appsflyer_id` / `adid` / `advertising_id` / `device_id`（AppsFlyer / Adjust SDK 生成的归因与设备标识），服务端据此向 MMP 上报注册与首充事件；H5 必须在页面跳转 / 登录 / 注册全程保留并**原样**传入，缺 `adid` 与 `appsflyer_id` 时服务端不上报（AppsFlyer 链路还需 `app_id`）
area_code | string | 否 | 国家手机区号（纯数字，如 `55`）。商户已配置区号时，缺省则服务端填充；若传入必须等于商户配置值。最大 8 字符

**当 `account_type` 省略或为 `phone` 时**

- `username` 即手机号，同时写入会员的 `username` 与 `phone` 字段，**同商户内手机号唯一**
- **手机号校验说明**：仅当商户在总后台配置了国家手机区号时，才用对应正则校验 `username` 并校验 `area_code` 与商户配置一致；商户未配置区号则**区号与号码格式均不校验**（兼容存量商户）

**当 `account_type=email` 时**

- `username` 传邮箱；服务端将其**归一化**（去首尾空格 + 转小写）后存入 `email` 字段作为登录标识，因此 `A@B.com` 与 `a@b.com` 视为**同一账号**
- 邮箱格式非法返回业务错误 `邮箱格式不正确`；**同商户内邮箱唯一**（与其他会员填写的 PIX 收款邮箱共用同一唯一空间）
- 不做任何区号 / 手机号格式校验，`phone` 字段留空
- 注册**不发送**验证邮件，`email_verified` 保持 `false`（见 [Member 对象](#member)）

**请求示例 —— 手机号注册（与旧版本完全一致，无需改动）：**

```bash
curl -X POST 'https://<your-domain>/api/game/auth/register' \
  -H 'Content-Type: application/json' \
  -d '{
    "username": "11987654321",
    "password": "abc12345",
    "area_code": "55",
    "source": "h5"
  }'
```

**请求示例 —— 邮箱注册：**

```bash
curl -X POST 'https://<your-domain>/api/game/auth/register' \
  -H 'Content-Type: application/json' \
  -d '{
    "username": "Player@Example.com",
    "password": "abc12345",
    "account_type": "email",
    "source": "h5"
  }'
```

**输出:**

字段 | 类型 | 说明
---|---|---
token | string | access jwt token
user_id | string | 会员 ID

**业务错误:**

message | 触发条件
---|---
手机区号与商户配置不一致 | 客户端传入的 `area_code` 与商户配置值不一致
手机号格式不正确 | 商户已配置区号时，`username`（=手机号）不匹配商户区号对应的正则
邮箱格式不正确 | `account_type=email` 时 `username` 不是合法邮箱地址
该手机号已被注册 | 同商户内该手机号已存在
该邮箱已被注册 | 同商户内该邮箱已存在（按归一化后的值比对）
账号已存在 | 同商户内 `username` 原值已被占用
注册失败，请重试 | 并发注册同一账号时命中唯一约束的兜底提示，可换账号或重试操作过于频繁，请稍后再试 | 同一 IP 1 分钟内调用本接口超过 10 次（无论成功与否）
---

## 登录 {#login}

**URL:** `POST` /auth/login

**输入:**

字段 | 类型 | 必填 | 说明
---|---|---|---
username | string | 是 | 账号，长度 2-64。`login_type=phone` 时传**手机号**（纯号码，不含区号）；`login_type=email` 时传**邮箱**；`login_type=guest` 时传【[游客注册](#guest_register)】返回的 `username`
password | string | 是 | 密码，长度 4-64。`login_type=guest` 时传【[游客注册](#guest_register)】返回的 `password`
login_type | string | 否 | 登录方式：`phone` 手机号 / `email` 邮箱 / `guest` 游客。**留空按 `phone` 处理**，旧客户端不传时行为与之前完全一致；传枚举以外的值会被参数校验拦截（`400`）

**当 `login_type` 省略或为 `phone` 时**

- 先按 `phone` 字段匹配；未命中时**自动降级**按 `username` 匹配，因此存量会员与未配置区号商户的会员照常登录

**当 `login_type=email` 时**

- 输入邮箱先归一化（去首尾空格 + 转小写）再匹配 `email` 字段，**大小写不敏感**
- 登录**不检查** `email_verified` / `phone_verified`：这两个字段仅用于提款等敏感操作的风控，与能否登录无关

**当 `login_type=guest` 时**

- 按 `username` 精确匹配，用于客户端用本机保存的游客账号密码**自动登录**（玩家不填任何东西）
- 游客已通过【[绑定账号](#bind_account)】转正后，游客密码已被用户自填密码覆盖，本机那副旧凭证再登录会返回 `账号或密码错误`——客户端应在转正成功时立即清除本机游客凭证，不给免密进会员账号留后门

**请求示例 —— 邮箱登录：**

```bash
curl -X POST 'https://<your-domain>/api/game/auth/login' \
  -H 'Content-Type: application/json' \
  -d '{
    "username": "Player@Example.com",
    "password": "abc12345",
    "login_type": "email"
  }'
```

**输出:**

字段 | 类型 | 说明
---|---|---
token | string | access jwt token
user_id | string | 会员 ID

**业务错误:**

message | 触发条件
---|---
账号或密码错误 | 账号不存在**或**密码错误。两种情况返回**同一文案**（防账号枚举），⚠️ 客户端**不得**据此判断账号是否已注册
会员已被禁用 | 账号密码正确，但该会员已被商户停用
操作过于频繁，请稍后再试 | 登录**失败**次数超限：同一账号 10 分钟内失败 ≥ 10 次，或同一 IP 10 分钟内失败 ≥ 50 次；窗口内该账号 / IP 的后续登录均被拒绝，成功登录不计数

---

## 游客注册 {#guest_register}

免填表开号：**客户端自行随机生成**一副账号密码提交，服务端据此创建游客账号并直接签发登录 token。玩家一个字不用填，也不经手这副账号密码。**免登录**；需商户在后台开启「允许游客登录」（**默认关闭**）。

**URL:** `POST` /auth/guest_register

**输入:**

字段 | 类型 | 必填 | 说明
---|---|---|---
username | string | 是 | 客户端随机生成的游客账号，长度 8-64，必须匹配 `^g[A-Za-z0-9]{7,63}$`（小写 `g` 开头 + 字母数字）。建议 `g` + 12 位以上随机字母数字，商户内唯一；撞名返回 `账号已存在`，客户端重新生成再试
password | string | 是 | 客户端随机生成的游客密码，长度 8-64。建议用 `crypto.getRandomValues` 生成 32 位以上
promoter_code | string | 否 | 邀请人的推广码，长度不超过 50
channel_id | string | 否 | 渠道 ID，长度不超过 50
source | string | 否 | 来源标记，pwa、h5 等，长度不超过 100；传 `pwa` 时服务端同时标记为 PWA 用户（其它场景见【[上报 PWA 安装态](110_pwa.md#activate)】）
link_queries | string | 否 | 落地页 URL 的完整 query string，长度不超过 1024，语义与【[注册](#register)】相同（解析 `fbclid` 用于广告归因；APK / IPA 场景的 MMP 归因参数同样须原样透传）

**行为说明**

- 每次调用都新开一个账号：`username` / `password` 原值落库（密码哈希存储），`register_type` = `guest`，`is_guest` = `true`
- ✅ **客户端必须在调用前把这副 `username` / `password` 持久化到本机**（与伪设备号同存同退）：它们是该游客账号唯一的登录凭证，服务端**不会**在任何接口返回密码；丢失即无法再登录该账号，泄露即他人可登录。玩家不经手也不可见，客户端不要展示
- **游客登录**：本机存有这副凭证时，调【[登录](#login)】传 `login_type=guest` + 保存的 `username` / `password` 自动登录，用回原账号（余额、奖金、记录都在）；本机没存才生成新凭证调本接口开新号。✅ 不要每次启动都调本接口，否则会不断造新号
- 游客账号特性（【[Member 对象](#member)】`is_guest=true`）：
    - **不可提现**：【[创建提现订单（统一）](200_finance.md#withdraw_create)】与【[创建 PIX 提现](200_finance.md#withdraw_pix_create)】返回业务错误 `游客账号不支持提现，请先绑定正式账号`
    - **拥有免费账户（体验金）**：【[免费注册奖金](418_promotion_free_register_bonus.md)】开启时，本接口即是该活动的**唯一领取入口**：注册事务内一次性发放、用完不补，余额见【[账户余额](200_finance.md#account_balance)】的 `free_money`；可用【[免费账户启动游戏](300_game.md#launch_free)】试玩（目前仅 365 平台游戏）。免费余额与真实余额完全隔离，不可提现、不计打码、不触发活动。调用前建议先调【[领取资格](418_promotion_free_register_bonus.md#eligibility)】
    - 通过【[绑定账号](#bind_account)】绑定手机/邮箱并设置密码后**转正**（`is_guest` 变为 `false`），解除上述提现限制；**转正不影响免费账户**（余额、倒计时、活动状态均不变，免费账户的锁定/拆笔只由充值驱动）；转正时游客密码被用户自填密码覆盖，**本机那副旧凭证随即失效**，客户端应同时清除它
- 并发造号防护：同一 IP 1 分钟内超过 10 次即拒绝（见下方业务错误）
- 其余登录后行为（token 有效期、`/auth/detail` 续签等）与密码登录完全一致

**请求示例：**

```bash
curl -X POST 'https://<your-domain>/api/game/auth/guest_register' \
  -H 'Content-Type: application/json' \
  -d '{
    "username": "g7k2m9x4qp1z",
    "password": "Zt8vQ2nL5wR9kX3mB7cD1fH4jP6sV0yA",
    "source": "pwa"
  }'
```

**输出:**

字段 | 类型 | 说明
---|---|---
token | string | access jwt token
user_id | string | 会员 ID

**业务错误:**

message | 触发条件
---|---
游客账号格式不正确 | `username` 不匹配 `^g[A-Za-z0-9]{7,63}$`（长度不合法则被参数校验拦截，`400`）
账号已存在 | 同商户内 `username` 已被占用，客户端重新生成再试
注册失败，请重试 | 并发注册同一账号时命中唯一约束的兑底提示，重新生成再试
当前商户未开放游客登录 | 商户未在后台开启「允许游客登录」
无效的推广码 | `promoter_code` 不存在
同IP注册人数已达上限 | 命中商户同 IP 注册数风控
当前网络领取免费奖金的次数已达上限 | 【[免费注册奖金](418_promotion_free_register_bonus.md)】开启且该 IP 领取次数已达 `ip_claim_limit`（非白名单），注册整体失败
操作过于频繁，请稍后再试 | 同一 IP 1 分钟内调用本接口超过 10 次（无论成功与否）

---

## 获取登录用户详情 {#detail}

**URL:** `GET` /auth/detail

**输出:**

字段 | 类型 | 说明
---|---|---
... | Member | 用户信息（含 `is_guest`，见【[Member 对象](#member)】）
new_token | string | 新签发的 JWT（可选，仅当当前 token 剩余有效期 < 30 天时返回）

> **Token 自动续签：** JWT 总有效期为 30 天。当服务端检测到当前 token 剩余有效期不足 30 天时，会在响应体顶层追加 `new_token` 字段，包含一个新签发的 JWT。前端收到该字段后应立即替换本地存储的 token，后续请求使用新 token。旧 token 不会被吊销，在自然过期前仍可使用。

---

## 绑定账号 {#bind_account}

为当前登录会员绑定手机号或邮箱。**需登录**。游客借此转正（同时设置密码）；普通会员用于补绑另一种账号。

**URL:** `POST` /auth/bind_account

**输入:**

字段 | 类型 | 必填 | 说明
---|---|---|---
account_type | string | 是 | 绑定类型：`phone` 手机号 / `email` 邮箱。传枚举以外的值会被参数校验拦截（`400`）
account | string | 是 | 账号，长度 2-64。`account_type=phone` 时传**手机号**（纯号码，不含区号）；`account_type=email` 时传**邮箱**
password | string | 条件 | 密码，长度 4-64。**游客必填**（转正同时设置密码）；**非游客必须留空**
area_code | string | 否 | 国家手机区号（纯数字，如 `55`），最大 8 字符，**仅 `account_type=phone` 时有意义**。商户已配置区号时，缺省则服务端填充；若传入必须等于商户配置值

**游客（`is_guest=true`）**

- 必须同时传 `password`，绑定成功即**转正**：`is_guest` 变为 `false`，解除提现限制
- 之后用【[登录](#login)】密码登录（`login_type` 对应 `phone` / `email`，`username` 传绑定的手机号 / 邮箱），换设备也能登录
- ⚠️ 传入的 `password` 会**覆盖**【[游客注册](#guest_register)】时客户端随机生成的游客密码，本机那副游客凭证随即失效（再用它 `login_type=guest` 登录返回 `账号或密码错误`）。客户端应在转正成功时立即清除本机游客凭证并隐藏「游客登录」入口
- `register_type` 与 `username` **不变**（仍为 `guest` / 注册时随机生成的用户名）——它们是注册来源，判断当前是否游客请以 `is_guest` 为准

**非游客**

- 仅补绑手机或邮箱，**不传** `password`（传了返回业务错误 `当前账号已设置密码，请勿重复提交`）
- 每个槽位只能绑一次：已有 `phone` 不可再绑手机，已有 `email` 不可再绑邮箱（含手机号注册会员因 [PIX 收款资料](200_finance.md#pix_create) 已写入的 `email`）

**校验规则（与【[注册](#register)】一致）**

- 手机号：仅当商户配置了区号时才校验号码格式与 `area_code` 一致性；商户未配置则不校验。**同商户内手机号唯一**
- 邮箱：服务端归一化（去首尾空格 + 转小写）后校验格式并存储；**同商户内邮箱唯一**（与其他会员的 PIX 收款邮箱共用同一唯一空间）
- 绑定**不发送**验证短信 / 邮件，`phone_verified` / `email_verified` 保持 `false`

**请求示例 —— 游客绑定手机号并设置密码（转正）：**

```bash
curl -X POST 'https://<your-domain>/api/game/auth/bind_account' \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer <jwt>' \
  -d '{
    "account_type": "phone",
    "account": "11987654321",
    "area_code": "55",
    "password": "abc12345"
  }'
```

**请求示例 —— 普通会员补绑邮箱：**

```bash
curl -X POST 'https://<your-domain>/api/game/auth/bind_account' \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer <jwt>' \
  -d '{
    "account_type": "email",
    "account": "Player@Example.com"
  }'
```

**输出:** 无

**业务错误:**

message | 触发条件
---|---
游客绑定账号需同时设置密码 | 游客调用时未传 `password`
当前账号已设置密码，请勿重复提交 | 非游客调用时传了 `password`
已绑定手机号，不可重复绑定 | `account_type=phone` 但会员 `phone` 已有值
已绑定邮箱，不可重复绑定 | `account_type=email` 但会员 `email` 已有值
手机区号与商户配置不一致 | 客户端传入的 `area_code` 与商户配置值不一致
手机号格式不正确 | 商户已配置区号时，`account` 不匹配商户区号对应的正则
邮箱格式不正确 | `account_type=email` 时 `account` 不是合法邮箱地址
该手机号已被注册 | 同商户内该手机号已被其他会员占用
该邮箱已被注册 | 同商户内该邮箱已被其他会员占用（按归一化后的值比对）
操作过于频繁，请稍后再试 | 同一会员 10 分钟内调用本接口超过 10 次（无论成功与否）

---

## 修改密码 {#change_password}

**URL:** `POST` /auth/change_password

**输入:**

字段 | 类型 | 必填 | 说明
---|---|---|---
old_password | string | 是 | 旧密码，长度 4-64
new_password | string | 是 | 新密码，长度 4-64，不能与旧密码相同

**输出:** 无

---

## 修改用户资料 {#update}

**URL:** `POST` /auth/update

**输入:**

字段 | 类型 | 必填 | 说明
---|---|---|---
nickname | string | 否 | 昵称，长度不超过 60
head_portrait | string | 否 | 头像 URL，长度不超过 1000；建议通过 [获取上传预签名 URL](050_common.md#get_upload_url) 上传后填入返回的公开访问地址

**输出:** 无

---

## 退出登录 {#logout}

**URL:** `POST` /auth/logout

**输入:** 无

**输出:** 无
