# LINE 登录回调 — 前端（uniapp）接入说明

> 配套后端：`LineCallbackController` → `GET /auth/line/callback`（2026-08-05 增量，017-oauth-login FR-009）。
> 适用场景：**「唤起 LINE App / 系统浏览器」** 授权 —— 这种方式前端拿不到 code，由后端接住 LINE 的重定向、完成登录后，再 302 跳回 App。
>
> 若你的场景是「H5 / 自家 webview / LINE SDK」（前端能自己拿到 code），**不走本回调**，直接调 `POST /infouser/user/oauthLogin {provider:"line", credential:code}`。两条流程并存。

---

## 1. 整体流程

```
① uniapp 打开 LINE 授权页（用 redirect_uri = https://api.awayqtw.com/auth/line/callback）
② 用户在 LINE App 里一键同意
③ LINE 跳转：GET https://api.awayqtw.com/auth/line/callback?code=xxx&state=xxx
④ 后端：code → 换 token → 取 userId → 查绑定 → 签 JWT 或 生成 tempKey
⑤ 后端 302 跳回 App scheme：
     com.twanmsdyh.app://oauthLogin?<参数>
⑥ uniapp 被 scheme 拉起，读参数，按下面三种情况处理
```

**关键：** 前端不碰 code，只接收后端 302 回来的 scheme URL 并按参数分流。

---

## 2. scheme 注册（manifest.json）

App 端要能被 `com.twanmsdyh.app://...` 拉起，需在 `manifest.json` 注册该 URL Scheme（HBuilderX：App 模块配置 → App 常用其它设置 / 各平台）：

```json
{
  "app-plus": {
    "distribute": {
      "android": { "schemes": "com.twanmsdyh.app" },
      "ios": { "urltypes": "com.twanmsdyh.app" }
    }
  }
}
```

> iOS 的 `urltypes` 填的是 scheme 名（不含 `://`）。注册后系统遇到 `com.twanmsdyh.app://...` 就会拉起你的 App 并把完整 URL 传进来。

---

## 3. 打开 LINE 授权页

前端用系统/LINE 打开如下 URL（`redirect_uri` **必须**与 LINE Console 白名单、后端 `application.yml` 三方一致）：

```
https://access.line.me/oauth2/v2.1/authorize
  ?response_type=code
  &client_id=2010911071
  &redirect_uri=https%3A%2F%2Fapi.awayqtw.com%2Fauth%2Fline%2Fcallback
  &scope=profile%20openid
  &state=<前端生成的随机串>
```

打开方式（uniapp App 端）：`plus.runtime.openURL(authorizeUrl)` 或 `plus.share.launchLaunch` / 系统 webview，交由 LINE App / 系统浏览器接管。

- `scope=profile openid`：后端 `v2/profile` 取 userId 要 `profile` 权限，**不能少**。
- `state`：前端随机生成，用于防 CSRF（后端当前**未强校验** state，但建议传，后续会加）。

---

## 4. 接收 302 回跳的 scheme URL

uniapp 在 `App.vue` 里读取拉起 App 的 URL：

```js
// App.vue
export default {
  onLaunch() {
    // #ifdef APP-PLUS
    // 冷启动：URL 落在 plus.runtime.arguments
    this.handleLineCallback(plus.runtime.arguments)
    // 热启动（App 已开，LINE 回跳再拉起）：监听 newargs
    plus.runtime.addEventListener('newargs', (e) => {
      this.handleLineCallback(e.args)
    })
    // #endif
  },
  methods: {
    handleLineCallback(url) {
      if (!url || !url.startsWith('com.twanmsdyh.app://oauthLogin')) return
      const queryStr = url.split('?')[1] || ''
      const params = this.parseQuery(queryStr)
      routeByLineParams(params)
    },
    parseQuery(q) {
      const o = {}
      q.split('&').forEach(kv => {
        const [k, v] = kv.split('=')
        if (k) o[k] = decodeURIComponent(v || '')
      })
      return o
    }
  }
}
```

> `plus.runtime.arguments` 的具体形态随 uniapp 版本略有差异（可能是完整 URL，也可能只是 query 段），联调时打印一次确认即可。建议先 `console.log('LINE cb url=', url)` 看真实值再解析。

---

## 5. 三种返回值处理（核心）

后端 302 回的 URL 形如 `com.twanmsdyh.app://oauthLogin?<参数>`，参数只有下列三种组合之一：

| 情况 | 参数 | 含义 | 前端动作 |
|------|------|------|----------|
| ✅ 已绑定、登录成功 | `?token=<JWT>` | 该 LINE 已绑过手机号账号，已签发 token | 存 token → 跳首页 |
| 🟡 未绑定、需绑手机 | `?needPhone=1&tempKey=<32位>` | 首次用该 LINE 登录，要绑手机号 | 跳「绑手机」页 → 收手机号+短信码 → 调 `/oauthBindPhone` |
| ❌ 失败 | `?error=<原因>` | 换 token 失败 / 账号停用 / 缺 code 等 | 提示并回登录页 |

### 5.1 `token` —— 登录成功

```js
function routeByLineParams(params) {
  if (params.token) {
    // 与现有手机号登录一致：token 存本地
    uni.setStorageSync('token', params.token)
    // 也可顺手把 cid/deviceToken 上报一次（后端回调时没有设备信息）
    reportDeviceInfo()
    uni.reLaunch({ url: '/pages/index/index' })
    return
  }
  // ...
}
```

- `token` 是后端签的 JWT（`Authorization` / `token` header 带它即可），与 `/lodeing` 返回的 token 同款，**直接复用现有请求拦截器**。
- 后端回调发生在服务端，**拿不到 cid/deviceToken**，故设备信息要么这里补报、要么等首次业务请求时按原流程更新。

### 5.2 `needPhone` + `tempKey` —— 需要绑手机

```js
if (params.needPhone === '1' && params.tempKey) {
  // tempKey 5 分钟有效，跳到绑手机页把它带上
  uni.navigateTo({ url: '/pages/login/bindPhone?tempKey=' + params.tempKey })
  return
}
```

绑手机页：输入手机号 → 调 `GET /infouser/user/getcode?phone=<手机号>` 发短信 → 填验证码 → 调下面接口完成绑定+登录：

### 5.3 `error` —— 失败

```js
if (params.error) {
  const map = {
    no_code: '回调缺少授权码，请重试',
    user_stopped: '该账号已被停用',
    fail: 'LINE 登录失败，请重试'
  }
  uni.showModal({ title: '登录失败', content: map[params.error] || decodeURIComponent(params.error), showCancel: false })
  uni.reLaunch({ url: '/pages/login/login' })
  return
}
```

- `error` 可能是固定码（`no_code` / `user_stopped`），也可能是后端异常文案（已 URL 编码，`decodeURIComponent` 解开）。

---

## 6. 绑定接口契约：`POST /infouser/user/oauthBindPhone`

`needPhone` 分支收集完手机号 + 短信码后调用。

**请求体（JSON）：**
```json
{
  "tempKey": "5.2 拿到的 tempKey",
  "phone":   "+8869xxxxxxxx",
  "code":    "短信验证码（万能码 8888 可用，仅测试）",
  "cid":         "推送 cid（个推/uni 推送）",
  "cidType":     "推送类型，与现有登录一致（本项目一般传 \"\"）",
  "deviceToken": "APNs deviceToken（iOS）",
  "voIPToken":   "VoIP push token（iOS）"
}
```

**响应：**
- 成功（已注册关联 / 未注册新建 + 写绑定）：
  ```json
  {
    "code": 200,
    "msg":  "登录成功",
    "data": { ...用户信息... },
    "token": "<JWT>"
  }
  ```
  → 存 `token` → 跳首页（同 5.1）。
- 失败：`code != 200`，常见 `msg`：短信码错误（`no.user.jcaptcha.error`）、账号停用（`no.user.stop`）、tempKey 过期（`no.oauth.tempkey.expired`，需重新走一遍 LINE 登录拿新 tempKey）。

> 绑定成功后 `info_user_oauth(user_id, provider="line", provider_uid=<LINE userId>)` 已写入；**下次同一 LINE 登录就走 5.1 的 `token` 分支**，不再要绑手机。

---

## 7. 注意事项

1. **redirect_uri 三方一致铁律**：前端 authorize 里的 `redirect_uri`、后端 `application.yml` 的 `oauth.line.redirect-uri`、LINE Console 回调白名单，三者必须**完全相同**（当前 = `https://api.awayqtw.com/auth/line/callback`），否则 LINE 回 `400 redirect_uri_mismatch`。
2. **tempKey 有效期 5 分钟**：超时后 `/oauthBindPhone` 回 `tempkey.expired`，需重新点 LINE 登录拿新的。
3. **302 到自定义 scheme 的兼容性**：标准系统浏览器会跟随 302 到 `com.twanmsdyh.app://...` 拉起 App。若联调发现 **LINE 内置浏览器**不跟随 302（App 没被拉起），后端可改为返回 HTML（`meta refresh` + 手动「打开 App」链接）兜底 —— 这是待联调确认项，目前是 302。
4. **设备信息**：后端回调链路没有 App 上下文，`cid` / `deviceToken` / `voIPToken` 在回调时更新不到；绑手机走 `/oauthBindPhone` 时会带上，已绑定的老用户建议 5.1 拿到 token 后补报一次（或复用既有设备上报逻辑）。
5. **state 暂未校验**：后端目前不验证 `state`，前端仍建议生成并传，后续后端会加 CSRF 校验。
6. **与 `/oauthLogin` 并存**：如果某端（如 H5）前端能自己拿到 code，直接 `POST /infouser/user/oauthLogin {provider:"line", credential:code, ...}`，不必走本回调；返回值结构与本回调的 `token` / `needPhone+tempKey` 对应一致。
