|
|
@@ -0,0 +1,508 @@
|
|
|
+## 概述
|
|
|
+
|
|
|
+货拉拉开放平台采用标准的 OAuth 2.0 `Authorization Code` 模式进行商户授权。
|
|
|
+此文档详细说明了授权流程、API接口、参数说明以及在项目中的集成方法。
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 目录
|
|
|
+
|
|
|
+1. [授权流程](#授权流程)
|
|
|
+2. [API 接口](#api-接口)
|
|
|
+3. [环境配置](#环境配置)
|
|
|
+4. [使用示例](#使用示例)
|
|
|
+5. [常见问题](#常见问题)
|
|
|
+6. [参考资源](#参考资源)
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 授权流程
|
|
|
+
|
|
|
+### Authorization Code 流程
|
|
|
+
|
|
|
+货拉拉的 OAuth 2.0 授权采用三步流程:
|
|
|
+
|
|
|
+```
|
|
|
+┌─────────────────┐ ┌──────────────┐ ┌────────────────┐
|
|
|
+│ 第三方平台 │ │ 货拉拉授权页 │ │ 货拉拉授权服务器 │
|
|
|
+│ (ISV) │ │ │ │ │
|
|
|
+└────────┬────────┘ └──────────────┘ └────────────────┘
|
|
|
+ │ │ │
|
|
|
+ │ 1. 跳转到授权页面 │ │
|
|
|
+ │ (generateAuthUrl) │ │
|
|
|
+ │─────────────────────────>│ │
|
|
|
+ │ │ │
|
|
|
+ │ 用户授权 │
|
|
|
+ │ │ │
|
|
|
+ │ │ 2. 返回授权码(code) │
|
|
|
+ │<──────────────────────────┤<─────────────────────────┤
|
|
|
+ │ redirect_uri?code=xxx │ │
|
|
|
+ │ │ │
|
|
|
+ │ 3. 使用code换取token │ │
|
|
|
+ │ (getAccessToken) │ │
|
|
|
+ │────────────────────────────────────────────────────>│
|
|
|
+ │ │ │
|
|
|
+ │ │ 返回 access_token │
|
|
|
+ │<────────────────────────────────────────────────────│
|
|
|
+ │ │ │
|
|
|
+```
|
|
|
+
|
|
|
+### 流程步骤详解
|
|
|
+
|
|
|
+#### 步骤1:生成授权URL
|
|
|
+
|
|
|
+商户系统中生成授权链接,引导用户跳转到货拉拉授权页面:
|
|
|
+
|
|
|
+```php
|
|
|
+$auth = new \common\components\delivery\platform\huolala\Auth();
|
|
|
+$redirectUrl = 'https://your-domain.com/delivery/huolala-callback';
|
|
|
+$authUrl = $auth->generateAuthUrl($redirectUrl);
|
|
|
+
|
|
|
+// 在页面中跳转
|
|
|
+Yii::$app->response->redirect($authUrl);
|
|
|
+```
|
|
|
+
|
|
|
+**生成的URL格式:**
|
|
|
+```
|
|
|
+https://open.huolala.cn/#/oauth/authorize?response_type=code&client_id=xxx&redirect_uri=xxx&isSandbox=true
|
|
|
+```
|
|
|
+
|
|
|
+#### 步骤2:用户授权
|
|
|
+
|
|
|
+用户在货拉拉授权页面填写账号信息并同意授权。货拉拉会重定向回 `redirect_uri` 并携带授权码 `code` 参数。
|
|
|
+
|
|
|
+#### 步骤3:交换Access Token
|
|
|
+
|
|
|
+在回调页面获取 `code`,调用接口交换 Access Token:
|
|
|
+
|
|
|
+```php
|
|
|
+$code = Yii::$app->request->get('code');
|
|
|
+$auth = new \common\components\delivery\platform\huolala\Auth();
|
|
|
+$result = $auth->getAccessToken($code);
|
|
|
+
|
|
|
+if ($result['success']) {
|
|
|
+ // 保存到数据库
|
|
|
+ $accessToken = $result['access_token'];
|
|
|
+ $refreshToken = $result['refresh_token'];
|
|
|
+ $expiresIn = $result['expires_in']; // 3个月
|
|
|
+} else {
|
|
|
+ // 处理错误
|
|
|
+ $error = $result['error'];
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## API 接口
|
|
|
+
|
|
|
+### 1. 生成授权URL
|
|
|
+
|
|
|
+**方法:** `generateAuthUrl($redirectUri)`
|
|
|
+
|
|
|
+**说明:** 生成用户授权的URL,用户通过访问此URL进行授权。
|
|
|
+
|
|
|
+**参数:**
|
|
|
+
|
|
|
+| 参数 | 类型 | 必须 | 说明 |
|
|
|
+|------|------|------|------|
|
|
|
+| redirectUri | string | 是 | 授权回调地址,用户授权后会跳转到此地址 |
|
|
|
+
|
|
|
+**返回值:** 授权URL字符串
|
|
|
+
|
|
|
+**示例:**
|
|
|
+
|
|
|
+```php
|
|
|
+$auth = new Auth();
|
|
|
+$authUrl = $auth->generateAuthUrl('http://your-domain.com/callback');
|
|
|
+// https://open.huolala.cn/#/oauth/authorize?response_type=code&client_id=xxx&redirect_uri=xxx&isSandbox=true
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### 2. 获取Access Token
|
|
|
+
|
|
|
+**方法:** `getAccessToken($code)`
|
|
|
+
|
|
|
+**说明:** 使用授权码获取访问令牌和刷新令牌。
|
|
|
+
|
|
|
+**接口:**
|
|
|
+- 路径:`/oauth/token`
|
|
|
+- 请求方式:`GET`
|
|
|
+- 环境:根据 `YII_ENV` 自动判断沙箱或生产环境
|
|
|
+
|
|
|
+**请求参数:**
|
|
|
+
|
|
|
+| 参数 | 类型 | 必须 | 说明 |
|
|
|
+|------|------|------|------|
|
|
|
+| grant_type | string | 是 | 固定值:`authorization_code` |
|
|
|
+| client_id | string | 是 | 应用 App Key |
|
|
|
+| code | string | 是 | 授权码(来自授权回调) |
|
|
|
+| isSandbox | boolean | 是 | 沙箱环境标志(自动处理) |
|
|
|
+
|
|
|
+**返回值:**
|
|
|
+
|
|
|
+```php
|
|
|
+[
|
|
|
+ 'success' => true, // 请求是否成功
|
|
|
+ 'access_token' => 'xxx', // 访问令牌
|
|
|
+ 'refresh_token' => 'yyy', // 刷新令牌
|
|
|
+ 'expires_in' => 7776000, // 过期时间(秒),3个月
|
|
|
+]
|
|
|
+```
|
|
|
+
|
|
|
+**示例:**
|
|
|
+
|
|
|
+```php
|
|
|
+$auth = new Auth();
|
|
|
+$code = Yii::$app->request->get('code');
|
|
|
+$result = $auth->getAccessToken($code);
|
|
|
+
|
|
|
+if ($result['success']) {
|
|
|
+ echo "Token: " . $result['access_token'];
|
|
|
+} else {
|
|
|
+ echo "Error: " . $result['error'];
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### 3. 刷新Access Token
|
|
|
+
|
|
|
+**方法:** `refreshAccessToken($refreshToken)`
|
|
|
+
|
|
|
+**说明:** 使用刷新令牌获取新的访问令牌,无需用户重新授权。
|
|
|
+
|
|
|
+**接口:**
|
|
|
+- 路径:`/oauth/token`
|
|
|
+- 请求方式:`GET`
|
|
|
+- 环境:根据 `YII_ENV` 自动判断沙箱或生产环境
|
|
|
+
|
|
|
+**请求参数:**
|
|
|
+
|
|
|
+| 参数 | 类型 | 必须 | 说明 |
|
|
|
+|------|------|------|------|
|
|
|
+| grant_type | string | 是 | 固定值:`refresh_token` |
|
|
|
+| client_id | string | 是 | 应用 App Key |
|
|
|
+| refresh_token | string | 是 | 刷新令牌 |
|
|
|
+| isSandbox | boolean | 是 | 沙箱环境标志(自动处理) |
|
|
|
+
|
|
|
+**返回值:**
|
|
|
+
|
|
|
+```php
|
|
|
+[
|
|
|
+ 'success' => true, // 请求是否成功
|
|
|
+ 'access_token' => 'xxx', // 新的访问令牌
|
|
|
+ 'refresh_token' => 'yyy', // 新的刷新令牌
|
|
|
+ 'expires_in' => 7776000, // 新的过期时间(秒)
|
|
|
+]
|
|
|
+```
|
|
|
+
|
|
|
+**示例:**
|
|
|
+
|
|
|
+```php
|
|
|
+$auth = new Auth();
|
|
|
+$result = $auth->refreshAccessToken($oldRefreshToken);
|
|
|
+
|
|
|
+if ($result['success']) {
|
|
|
+ // 使用新的 token
|
|
|
+ $newAccessToken = $result['access_token'];
|
|
|
+ $newRefreshToken = $result['refresh_token'];
|
|
|
+} else {
|
|
|
+ echo "Error: " . $result['error'];
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 环境配置
|
|
|
+
|
|
|
+### 环境变量
|
|
|
+
|
|
|
+**YII_ENV 变量的作用:**
|
|
|
+
|
|
|
+| 值 | 说明 | isSandbox | 基础URL |
|
|
|
+|-----|------|----------|--------|
|
|
|
+| production | 正式环境 | false | https://open.huolala.cn |
|
|
|
+| 其他 | 沙箱/测试环境 | true | https://open.huolala.cn(同一个域名,通过参数区分) |
|
|
|
+
|
|
|
+### 沙箱环境测试
|
|
|
+
|
|
|
+**沙箱测试账户:**
|
|
|
+- 验证码固定为:`6666`
|
|
|
+- 环境:`isSandbox=true`
|
|
|
+
|
|
|
+**切换到沙箱环境:**
|
|
|
+
|
|
|
+```bash
|
|
|
+# 设置环境变量
|
|
|
+export YII_ENV=dev
|
|
|
+
|
|
|
+# 或在 env.php 中配置
|
|
|
+define('YII_ENV', 'dev');
|
|
|
+```
|
|
|
+
|
|
|
+### 配置应用凭证
|
|
|
+
|
|
|
+在 `Auth.php` 构造函数中修改:
|
|
|
+
|
|
|
+```php
|
|
|
+// 生产环境配置
|
|
|
+$this->appKey = '正式环境的app_key';
|
|
|
+$this->appSecret = '正式环境的app_secret';
|
|
|
+
|
|
|
+// 沙箱环境配置
|
|
|
+$this->appKey = '沙箱环境的app_key';
|
|
|
+$this->appSecret = '沙箱环境的app_secret';
|
|
|
+```
|
|
|
+
|
|
|
+或通过环境变量注入(推荐):
|
|
|
+
|
|
|
+```php
|
|
|
+public function __construct()
|
|
|
+{
|
|
|
+ $isProduction = getenv('YII_ENV') == 'production';
|
|
|
+
|
|
|
+ if ($isProduction) {
|
|
|
+ $this->appKey = getenv('HUOLALA_PROD_APP_KEY');
|
|
|
+ $this->appSecret = getenv('HUOLALA_PROD_APP_SECRET');
|
|
|
+ } else {
|
|
|
+ $this->appKey = getenv('HUOLALA_SANDBOX_APP_KEY');
|
|
|
+ $this->appSecret = getenv('HUOLALA_SANDBOX_APP_SECRET');
|
|
|
+ }
|
|
|
+
|
|
|
+ $this->isSandbox = !$isProduction;
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 使用示例
|
|
|
+
|
|
|
+### 完整的授权流程
|
|
|
+
|
|
|
+```php
|
|
|
+<?php
|
|
|
+namespace app\controllers;
|
|
|
+
|
|
|
+use common\components\delivery\platform\huolala\Auth;
|
|
|
+use Yii;
|
|
|
+
|
|
|
+class DeliveryController extends \yii\web\Controller
|
|
|
+{
|
|
|
+ /**
|
|
|
+ * 步骤1:跳转到授权页面
|
|
|
+ */
|
|
|
+ public function actionAuthorizeHuolala()
|
|
|
+ {
|
|
|
+ $auth = new Auth();
|
|
|
+ $redirectUrl = Yii::$app->urlManager->createAbsoluteUrl(['delivery/huolala-callback']);
|
|
|
+ $authUrl = $auth->generateAuthUrl($redirectUrl);
|
|
|
+
|
|
|
+ return Yii::$app->response->redirect($authUrl);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 步骤2:处理授权回调
|
|
|
+ */
|
|
|
+ public function actionHuoLalaCallback()
|
|
|
+ {
|
|
|
+ $code = Yii::$app->request->get('code');
|
|
|
+
|
|
|
+ if (!$code) {
|
|
|
+ return $this->redirect(['index', 'error' => '授权失败']);
|
|
|
+ }
|
|
|
+
|
|
|
+ $auth = new Auth();
|
|
|
+ $result = $auth->getAccessToken($code);
|
|
|
+
|
|
|
+ if (!$result['success']) {
|
|
|
+ return $this->redirect(['index', 'error' => $result['error']]);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 保存 token 到数据库
|
|
|
+ $this->saveHuoLalaToken([
|
|
|
+ 'access_token' => $result['access_token'],
|
|
|
+ 'refresh_token' => $result['refresh_token'],
|
|
|
+ 'expires_at' => time() + $result['expires_in'],
|
|
|
+ ]);
|
|
|
+
|
|
|
+ return $this->redirect(['index', 'success' => '授权成功']);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 刷新过期的 Token
|
|
|
+ */
|
|
|
+ public function actionRefreshHuoLalaToken()
|
|
|
+ {
|
|
|
+ // 从数据库获取存储的 refresh_token
|
|
|
+ $storedToken = $this->getHuoLalaToken();
|
|
|
+
|
|
|
+ if (!$storedToken || !$storedToken['refresh_token']) {
|
|
|
+ throw new \Exception('未找到有效的刷新令牌');
|
|
|
+ }
|
|
|
+
|
|
|
+ $auth = new Auth();
|
|
|
+ $result = $auth->refreshAccessToken($storedToken['refresh_token']);
|
|
|
+
|
|
|
+ if (!$result['success']) {
|
|
|
+ throw new \Exception('Token 刷新失败:' . $result['error']);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 更新 token
|
|
|
+ $this->saveHuoLalaToken([
|
|
|
+ 'access_token' => $result['access_token'],
|
|
|
+ 'refresh_token' => $result['refresh_token'],
|
|
|
+ 'expires_at' => time() + $result['expires_in'],
|
|
|
+ ]);
|
|
|
+
|
|
|
+ return $result;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 保存 Token
|
|
|
+ */
|
|
|
+ private function saveHuoLalaToken($tokenData)
|
|
|
+ {
|
|
|
+ // 实现数据库保存逻辑
|
|
|
+ // ...
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取 Token
|
|
|
+ */
|
|
|
+ private function getHuoLalaToken()
|
|
|
+ {
|
|
|
+ // 实现数据库获取逻辑
|
|
|
+ // ...
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+### 处理Token过期
|
|
|
+
|
|
|
+```php
|
|
|
+<?php
|
|
|
+/**
|
|
|
+ * 检查 Token 是否即将过期,并自动刷新
|
|
|
+ */
|
|
|
+class HuoLalaTokenManager
|
|
|
+{
|
|
|
+ const REFRESH_THRESHOLD = 7 * 24 * 3600; // 提前7天刷新
|
|
|
+
|
|
|
+ public static function ensureTokenValid()
|
|
|
+ {
|
|
|
+ $token = self::getToken();
|
|
|
+
|
|
|
+ // 检查是否即将过期
|
|
|
+ if ($token['expires_at'] - time() < self::REFRESH_THRESHOLD) {
|
|
|
+ $auth = new Auth();
|
|
|
+ $result = $auth->refreshAccessToken($token['refresh_token']);
|
|
|
+
|
|
|
+ if ($result['success']) {
|
|
|
+ self::saveToken([
|
|
|
+ 'access_token' => $result['access_token'],
|
|
|
+ 'refresh_token' => $result['refresh_token'],
|
|
|
+ 'expires_at' => time() + $result['expires_in'],
|
|
|
+ ]);
|
|
|
+
|
|
|
+ return $result['access_token'];
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return $token['access_token'];
|
|
|
+ }
|
|
|
+
|
|
|
+ private static function getToken()
|
|
|
+ {
|
|
|
+ // 从数据库获取
|
|
|
+ }
|
|
|
+
|
|
|
+ private static function saveToken($data)
|
|
|
+ {
|
|
|
+ // 保存到数据库
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 常见问题
|
|
|
+
|
|
|
+### Q1: 授权码(code)的有效期是多久?
|
|
|
+
|
|
|
+**A:** 授权码有效期为 1 分钟,且只能使用一次。超期或多次使用都会失效。
|
|
|
+
|
|
|
+### Q2: Access Token 有效期是多久?
|
|
|
+
|
|
|
+**A:** Access Token 的有效期为 3 个月(7776000 秒)。当即将过期时,使用 `Refresh Token` 进行更新。
|
|
|
+
|
|
|
+### Q3: 如何切换生产环境和沙箱环境?
|
|
|
+
|
|
|
+**A:** 通过 `YII_ENV` 环境变量控制:
|
|
|
+- `YII_ENV=production` 为生产环境(`isSandbox=false`)
|
|
|
+- 其他值为沙箱环境(`isSandbox=true`)
|
|
|
+
|
|
|
+### Q4: 刷新 Token 时会改变 Refresh Token 吗?
|
|
|
+
|
|
|
+**A:** 是的,刷新 Token 后会同时返回新的 `access_token` 和 `refresh_token`。建议每次更新都保存新的 Refresh Token。
|
|
|
+
|
|
|
+### Q5: 授权时如何传递自定义参数?
|
|
|
+
|
|
|
+**A:** 可以在 `redirect_uri` 中添加查询参数,例如:
|
|
|
+```php
|
|
|
+$redirectUrl = 'https://your-domain.com/callback?shop_id=123&user_id=456';
|
|
|
+$authUrl = $auth->generateAuthUrl($redirectUrl);
|
|
|
+```
|
|
|
+
|
|
|
+回调时可以直接从 URL 中获取这些参数。
|
|
|
+
|
|
|
+### Q6: 如何处理 HTTP 请求失败?
|
|
|
+
|
|
|
+**A:** Auth 类会自动捕获异常并返回标准格式的错误响应:
|
|
|
+```php
|
|
|
+$result = $auth->getAccessToken($code);
|
|
|
+if (!$result['success']) {
|
|
|
+ $error = $result['error'];
|
|
|
+ $statusCode = $result['status_code'] ?? null;
|
|
|
+ // 处理错误
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 参考资源
|
|
|
+
|
|
|
+### 相关接口
|
|
|
+
|
|
|
+- **生成授权URL:** `generateAuthUrl($redirectUri)`
|
|
|
+- **获取Access Token:** `getAccessToken($code)`
|
|
|
+- **刷新Token:** `refreshAccessToken($refreshToken)`
|
|
|
+
|
|
|
+### 类方法
|
|
|
+
|
|
|
+- `getAppKey()` - 获取应用 App Key
|
|
|
+- `getAppSecret()` - 获取应用 App Secret
|
|
|
+- `isSandbox()` - 获取是否为沙箱环境
|
|
|
+
|
|
|
+### 官方文档
|
|
|
+
|
|
|
+- [货拉拉开放平台](https://open.huolala.cn)
|
|
|
+- OAuth 2.0 规范:https://tools.ietf.org/html/rfc6749
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 文件位置
|
|
|
+
|
|
|
+```
|
|
|
+common/components/delivery/platform/huolala/Auth.php
|
|
|
+```
|
|
|
+
|
|
|
+## 版本历史
|
|
|
+
|
|
|
+| 版本 | 日期 | 描述 |
|
|
|
+|------|------|------|
|
|
|
+| 1.0 | 2024-10-24 | 初始实现,支持 Authorization Code 流程 |
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+最后更新:2024-10-24
|