提示信息
# 同伴 App — 接口设计文档
> PHP 8.4 · Slim 4 · PostgreSQL · zircote/swagger-php v4
> 仅含 **App 系统模块** 与 **认证模块**,公共传参、签名规则、错误码见《后端开发规范》
---
## 5.1 App 系统模块 `/app`
### `POST /app/start` — 启动初始化(白名单)
#### 调用场景
App 进程冷启动时调用,是整个会话的第一个请求。主要覆盖以下几个时机:
- 用户首次安装后打开 App
- 用户手动杀进程后重新打开
- 系统内存不足将 App 杀掉后重新打开(iOS 的 `applicationDidBecomeActive` + 非 resume 场景 / Android 的 `Application.onCreate`)
**[业务逻辑]** 若已登录(Header 携带 Bearer Token),服务端会自动触发**每日自动签到**逻, 记录签到统计与流水。
已登录时需携带 Bearer Token,服务端据此填充 `user_status`;未登录时 Token 留空,`user_status` 全部返回 `false`。
#### 调用频率
**每次冷启动调用 1 次**,不重复调用。客户端需本地缓存响应结果(TTL 建议 1 小时),冷启动时先读缓存渲染,后台异步刷新更新本地缓存。App 切换到后台再回到前台属于热启动(resume),**不触发**此接口。
App 冷启动时调用,合并了配置下发 + 版本检查 + 用户状态,减少首屏请求次数。未登录时仅返回全局配置,已登录(携带 Bearer Token)时额外返回用户状态。
#### 请求体
```json
{
"platform": "android",
"version_code": 10234,
"channel": "huawei"
}
```
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `platform` | string | ✅ | `ios` / `android` |
| `version_code` | int | ✅ | 当前版本号(整型) |
| `channel` | string | — | 发行渠道,影响 `download_url` |
#### 响应示例
```json
{
"code": 0,
"msg": "ok",
"data": {
"review_mode": false,
"ad": {
"enable": false,
"config_version": "v2024060112",
"app_ids": {},
"strategies": {
"splash": [],
"reward_video": []
}
},
"update": {
"has_update": false,
"force_update": false,
"latest_version_code": 10234,
"latest_version_name": "3.2.0",
"release_notes": "",
"download_url": "",
"store_url": ""
},
"privacy": {
"version": "20240301",
"url": "https://example.com/privacy"
},
"features": {
"signal_daily_pull": 20,
"ad_pull_unlock": 5,
"signal_max": 5
},
"links": {
"user_agreement": "https://example.com/agreement",
"help_center": "https://example.com/help",
"feedback_h5": "https://example.com/feedback"
},
"user_status": {
"force_set_password": false,
"young_mode": false
}
},
"server_time": 1717200001
}
```
#### 字段说明
| 字段 | 类型 | 说明 |
|---|---|---|
| `review_mode` | bool | 审核模式,开启时隐藏广告、部分功能,避免商店审核被拒 |
| `ad.config_version` | string | 广告配置版本,客户端与本地缓存比较,变化时才刷新广告 SDK |
| `ad.strategies` | object | 各广告位的渠道优先级,`priority` 越小越优先 |
| `update.has_update` | bool | 是否有新版本可用 |
| `update.force_update` | bool | 是否强制升级(当前版本低于最低支持版本) |
| `update.download_url` | string | Android 直链;iOS 留空,用 `store_url` 跳商店 |
| `privacy.version` | string | 最新隐私协议版本号,客户端比对后决定是否弹授权窗 |
| `features` | object | 从 `config/app.php` 透传的业务参数,客户端读取避免硬编码 |
| `links` | object | H5 跳转链接,后台可随时修改免发版 |
| `user_status.force_set_password` | bool | 是否需要强制设置密码(未设密码且注册超 2 天) |
| `user_status.young_mode` | bool | 是否处于未成年人保护模式(已登录才有效) |
#### 开发备注
- 未登录时 `user_status` 所有字段返回 `false`,不报错。
- `update` 中的强制更新判断:`version_code < min_supported_version_code`,该阈值存 `app_versions` 表。
- 客户端本地缓存此接口结果,TTL 建议 1 小时;冷启动先读缓存展示,异步刷新后更新。
---
### `GET /app/config` — 获取核心配置(白名单)
#### 调用场景
不面向 App 客户端,主要用于以下两类场景:
- **运维健康检查**:负载均衡器或监控系统定期 ping,验证服务是否正常响应
- **开发调试**:快速确认当前部署的服务名称和版本号是否符合预期
App 客户端启动配置应调用 `/app/start`,不应直接调用此接口。
#### 调用频率
客户端不主动调用。运维侧按需调用,无频率限制要求。
轻量接口,仅返回应用名称和版本,用于 debug 或健康检查。
#### 响应示例
```json
{
"code": 0,
"msg": "ok",
"data": {
"app_name": "同伴",
"version": "3.2.0"
},
"server_time": 1717200001
}
```
| 字段 | 类型 | 说明 |
|---|---|---|
| `app_name` | string | 应用名称,来自 `config('app.name')` |
| `version` | string | 应用版本号,来自 `config('app.version')` |
---
### `GET /app/version` — 版本检查(白名单)
#### 调用场景
已从 `/app/start` 中分离出来作为独立接口,主要用于以下几个时机:
- **启动时**:`/app/start` 已内含版本检查结果,大多数情况下不需要单独调用此接口
- **设置页主动检查更新**:用户手动点击"检查更新"按钮时调用
#### 调用频率
- 启动时:通常由 `/app/start` 覆盖,**不单独调用**
- 用户主动触发:每次点击检查更新调用 1 次
#### 请求参数(Query String)
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `platform` | string | ✅ | `ios` / `android` |
| `version_code` | int | ✅ | 当前版本号 |
| `channel` | string | — | 发行渠道 |
#### 响应示例
```json
{
"code": 0,
"msg": "ok",
"data": {
"has_update": true,
"force_update": false,
"latest_version_code": 10240,
"latest_version_name": "3.2.0",
"release_notes": "· 优化启动速度\n· 修复若干已知问题",
"download_url": "https://example.com/download/android/v3.2.0.apk",
"store_url": ""
},
"server_time": 1717200001
}
```
| 字段 | 类型 | 说明 |
|---|---|---|
| `has_update` | bool | 是否有新版本 |
| `force_update` | bool | 是否强制更新 |
| `release_notes` | string | 更新日志,`\n` 换行 |
| `download_url` | string | Android APK 直链 |
| `store_url` | string | iOS App Store 链接 |
---
### `POST /app/heartbeat` — 在线心跳(需登录)
#### 调用场景
用于维持用户在线状态,同时作为轻量的服务端主动推送通道,覆盖以下场景:
- **维持在线**:让其他用户(如聊天对象)能看到"对方在线"状态;Redis `user_online:{uid}` TTL 120s,心跳不续期则自然离线
- **未读数轮询**:顺带下发 `unread.message`、`unread.notification`,客户端无需额外发起查询请求
- **服务端下发指令**:通过 `commands` 字段实现轻量服务端推送,处理强制下线、系统公告弹窗等低频事件,比长轮询轻量、比 WebSocket 省资源
- **客户端校时**:`server_time` 用于修正客户端本地时间偏差,影响签名时间窗口判断
#### 调用频率
- App **在前台活跃时**:每 **30 秒**调用 1 次
- App **切换到后台**:立即停止,不再发送
- App **从后台恢复到前台**:立即补发 1 次,然后重启 30 秒定时器
- 服务端保护:同一 uid 间隔 < 10s 的请求静默忽略(不报错,不更新 Redis),防止异常客户端轰炸
> **注意**:用户未登录时不调用此接口。
> Redis key:`user_online:{uid}`,TTL 120s,自然过期即为离线。
#### 响应示例
```json
{
"code": 0,
"msg": "ok",
"data": {
"server_time": 1717200001,
"unread": {
"message": 3,
"notification": 1
},
"commands": []
},
"server_time": 1717200001
}
```
| 字段 | 类型 | 说明 |
|---|---|---|
| `server_time` | int | 服务端当前时间戳,用于客户端校时 |
| `unread.message` | int | 私信未读数 |
| `unread.notification` | int | 互动通知未读数(点赞/评论等) |
| `commands` | array | 服务端下发指令,如强制下线、弹窗公告 |
#### `commands` 结构示例
```json
[
{ "type": "force_logout", "reason": "账号在其他设备登录" },
{
"type": "show_dialog",
"title": "系统维护通知",
"content": "今晚 23:00-01:00 进行系统维护",
"actions": [{ "label": "知道了", "type": "dismiss" }]
}
]
```
#### 开发备注
- 心跳频率:前台活跃 30s 一次,切后台停止,回到前台立即补发一次。
- 服务端对同一 uid 做频率保护,< 10s 的请求静默忽略,不报错。
---
## 5.2 认证模块 `/auth`
### `POST /auth/sms/send` — 发送验证码(白名单)
#### 调用场景
任何需要短信验证码的操作前调用,通过 `scene` 区分场景,不同场景 Redis key 相互隔离:
- `login`:登录/注册页点击"获取验证码",最高频场景
- `reset_password`:找回密码流程,用户忘记密码时触发
- `bind_phone`:第三方账号(微信/Apple)绑定手机号时触发,或用户在设置页更换手机号
#### 调用频率
- **用户主动触发**:每次点击"获取验证码"按钮调用 1 次
- **重发**:客户端展示 60 秒倒计时,倒计时归零后用户可再次点击,即最短间隔 60 秒
- **服务端限流**:同手机号同场景 60s 内最多 1 次;同 IP 每小时最多 10 次(防刷)
#### 请求体
```json
{
"phone": "13800138000",
"scene": "login"
}
```
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `phone` | string | ✅ | 11 位手机号 |
| `scene` | string | ✅ | `login` / `reset_password` / `bind_phone` |
#### 响应示例
```json
{
"code": 0,
"msg": "ok",
"data": {
"expire_seconds": 300,
"retry_after_seconds": 60
},
"server_time": 1717200001
}
```
| 字段 | 类型 | 说明 |
|---|---|---|
| `expire_seconds` | int | 验证码有效期(秒),客户端用于倒计时 |
| `retry_after_seconds` | int | 重发冷却时间(秒),客户端禁用重发按钮 |
#### 开发备注
- 限流:同手机号 60s 内只允许发 1 次,同 IP 每小时上限 10 次。
- 验证码:6 位数字,有效期 5 分钟,最多失败验证 3 次后当次码作废。
- `scene` 隔离:各场景 Redis key 独立,防止登录码被用于改密场景。
---
### `POST /auth/login/phone` — 手机号登录(白名单)
#### 调用场景
用户在登录页完成手机号输入后提交时调用,覆盖以下几种情况:
- **新用户**:输入手机号 + 验证码,服务端自动注册并登录,`is_new_user=true`,客户端跳新手引导页
- **老用户验证码登录**:手机号 + 验证码,适合不记得密码或快速登录的场景
- **老用户密码登录**:手机号 + 密码,适合已设置密码的用户
#### 调用频率
**用户主动触发,每次登录调用 1 次**。在 Token 有效期内不会再次调用(续期走 `/auth/token/refresh`)。
支持验证码登录和密码登录,手机号不存在时自动注册。
#### 请求体(验证码登录)
```json
{
"phone": "13800138000",
"code": "123456",
"device_info": {
"device_id": "uuid-xxx",
"platform": "android",
"model": "Pixel 8",
"os_version": "14",
"app_version": "3.2.0",
"push_token": "fcm-token-xxx"
}
}
```
#### 请求体(密码登录)
```json
{
"phone": "13800138000",
"password": "your_password",
"device_info": { ... }
}
```
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `phone` | string | ✅ | 手机号 |
| `code` | string | — | 短信验证码,与 `password` 二选一 |
| `password` | string | — | 登录密码,与 `code` 二选一 |
| `device_info` | object | ✅ | 设备信息,用于推送绑定和风控 |
| `device_info.device_id` | string | ✅ | 客户端生成的唯一设备标识 |
| `device_info.push_token` | string | — | FCM / APNs 推送 token |
#### 响应示例
```json
{
"code": 0,
"msg": "ok",
"data": {
"is_new_user": false,
"access_token": "eyJhbGci...",
"refresh_token": "dGhpcyBp...",
"access_expires_in": 7200,
"refresh_expires_in": 2592000,
"im_token": "openim-token-xxx",
"user": {
"uid": 10086,
"nickname": "用户10086",
"avatar": "",
"gender": 0,
"is_verified": false,
"is_vip": false
}
},
"server_time": 1717200001
}
```
| 字段 | 类型 | 说明 |
|---|---|---|
| `is_new_user` | bool | 是否新注册,客户端用于决定是否跳新手引导 |
| `access_token` | string | 短期访问令牌,有效期来自 `config('jwt.expire')`,默认 2h |
| `refresh_token` | string | 长期刷新令牌,有效期来自 `config('jwt.refresh_expire')`,默认 30d |
| `access_expires_in` | int | access_token 剩余有效秒数 |
| `refresh_expires_in` | int | refresh_token 剩余有效秒数 |
| `im_token` | string | OpenIM 登录 token,客户端初始化 IM SDK 使用 |
| `user.uid` | int | 用户 ID |
| `user.is_verified` | bool | 是否完成实名认证 |
| `user.is_vip` | bool | 是否 VIP |
#### 开发备注
- Token 存储:客户端将 refresh_token 存入 Keychain(iOS)/ EncryptedSharedPreferences(Android)。
- `device_info` 写入 `user_devices` 表,记录登录日志到 `user_login_logs`。
### `POST /auth/token/refresh` — 刷新 Token(白名单)
#### 调用场景
客户端**静默调用**,用户无感知,覆盖以下触发时机:
- **主动续期**:App 冷启动时检测本地 `access_token` 剩余时间 < 30 分钟,在后台静默刷新
- **请求失败续期**:任意业务接口返回 `401`(Token 过期),客户端拦截器自动调用此接口换取新 Token,然后重放原请求
- **定时检查**:App 在前台长时间运行(如用户一直刷内容),建议在 `access_token` 到期前 10 分钟静默刷新,避免用户操作时突然 401
#### 调用频率
- 正常使用:`access_token` 默认有效期 2 小时,理论上每 **2 小时**触发 1 次静默刷新
- 服务端保护:同一 `refresh_token` 在极短时间内(< 3s)的重复请求应返回同一结果(幂等处理),防止客户端并发刷新时多次消耗 Rotation 配额
#### 请求体
```json
{
"refresh_token": "dGhpcyBp..."
}
```
#### 响应示例
```json
{
"code": 0,
"msg": "ok",
"data": {
"access_token": "eyJhbGci...",
"access_expires_in": 7200,
"refresh_token": "bmV3cmVm...",
"refresh_expires_in": 2592000,
"im_token": "eyJhbGci..."
},
"server_time": 1717200001
}
```
| 字段 | 类型 | 说明 |
|---|---|---|
| `access_token` | string | 新的访问令牌 |
| `refresh_token` | string | 新的刷新令牌(Rotation 机制,旧 token 立即失效) |
| `access_expires_in` | int | 新 access_token 有效秒数 |
| `refresh_expires_in` | int | 新 refresh_token 有效秒数 |
| `im_token` | string | **[可选]** 新的 OpenIM Token(如需同步刷新) |
#### 开发备注
- Refresh Token Rotation:每次刷新同时下发新 refresh_token,旧 token 加入 `token_blacklist:{jti}` 黑名单,可检测重放攻击。
- 客户端并发刷新保护:客户端需加锁,多个并发请求只发一次 refresh,其余等待复用新 token。
---
### `POST /auth/im/token` — 刷新 OpenIM Token(需登录)
#### 调用场景
当客户端 IM SDK 报告 Token 过期(ErrorCode: 10001)或开发者希望主动更新 IM 身份凭证时调用。
#### 调用频率
- 正常情况下,OpenIM Token 有效期较长(24h+),建议在接入层捕获 SDK 异常后按需调用。
#### 响应示例
```json
{
"code": 0,
"msg": "ok",
"data": {
"im_token": "eyJhbGci..."
},
"server_time": 1717200001
}
```
| 字段 | 类型 | 说明 |
|---|---|---|
| `im_token` | string | 新的 OpenIM Token |
---
### `POST /auth/logout` — 退出登录(需登录)
#### 调用场景
用户主动触发,覆盖以下几种情况:
- **设置页退出**:用户在"我的 → 设置 → 退出登录"点击确认后调用
- **切换账号**:用户要切换另一个账号时,先退出当前账号
- **被服务端强制下线**:心跳返回 `commands` 包含 `force_logout` 时,客户端收到后应立即调用此接口清理 Token,然后跳转到登录页
#### 调用频率
**用户主动触发,每次退出调用 1 次**,极低频。无需限流保护,但服务端应做幂等处理(Token 已在黑名单中时不报错,直接返回成功)。
#### 请求体
```json
{
"device_id": "uuid-xxx"
}
```
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `device_id` | string | — | 当前设备 ID,用于精确解绑推送 token |
#### 响应示例
```json
{
"code": 0,
"msg": "ok",
"data": {},
"server_time": 1717200001
}
```
#### 开发备注
- 服务端将当前 access_token 的 `jti` 写入 `token_blacklist:{jti}`,TTL = token 剩余有效期。
- 同时将当前 refresh_token 加入黑名单。
- 解绑该设备 `push_token`,避免退出后继续收推送。
- 客户端无论服务端是否成功,本地都必须清除所有 token 和用户缓存。
---
## 5.2 用户模块 `/user`
### `GET /user/profile/me` — 获取个人资料(需登录)
#### 调用场景
进入 "我的" 页面获取基本信息,或进入 "资料编辑" 页面获取当前字段值。
#### 响应示例
```json
{
"code": 0,
"msg": "ok",
"data": {
"user": {
"uid": 2,
"nickname": "测试用户",
"avatar": "https://...",
"gender": 1,
"age": 25,
"bio": "这是简介",
"city": "上海",
"residence": {
"province": "上海",
"city": "上海",
"district": "浦东新区"
},
"birth_date": "1999-01-01",
"orientation": 1,
"mbti": "INTJ",
"is_verified": false,
"is_vip": true
}
}
}
```
---
### `POST /user/profile/update` — 更新个人资料(需登录)
#### 调用场景
- **资料编辑页**:修改单项或多项资料
- **补全引导页**:注册后完善性别、生日等必填项
#### 业务逻辑
**支持部分更新(Partial Update)**。请求体中仅传入需要修改的字段即可,未传入的字段将保持不变。
#### 请求体
```json
{
"nickname": "新昵称",
"bio": "新的人生格言",
"gender": 2,
"birth_date": "2000-05-20",
"mbti": "ENFP",
"residence": {
"province": "北京",
"city": "北京"
}
}
```
| 字段 | 类型 | 说明 |
|---|---|---|
| `nickname` | string | 昵称 |
| `avatar` | string | 头像 URL |
| `gender` | int | 性别 (1-男, 2-女) |
| `birth_date` | string | 生日 (YYYY-MM-DD) |
| `bio` | string | 个人简介 |
| `mbti` | string | MBTI 人格类型 |
| `residence` | object | 常住地 JSON 对象 |
| `orientation` | int | 性取向 |
| `attribute` | int | 属性/角色 |
#### 响应示例
成功后返回最新的完整用户信息(同 `GET /user/profile/me` 的 data 结构)。
---
## 启动时序参考
```
App 冷启动
│
├─ 并行 ─────┬── POST /app/start (配置 + 版本 + 用户状态,一次拿齐)
│ └── 本地读取缓存 Token 判断登录态
│
├─ 已登录且 access_token 即将过期 ──── POST /auth/token/refresh
│
└─ 首屏渲染完成后 ──── 开始定时 POST /app/heartbeat(30s/次)
```