Auth.php 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274
  1. <?php
  2. namespace common\components\delivery\platform\huolala;
  3. use common\components\delivery\helpers\HttpClient;
  4. use Yii;
  5. /**
  6. * 货拉拉商户授权
  7. *
  8. * 支持 OAuth2.0 Authorization Code 模式,遵循货拉拉开放平台授权规范。
  9. * Access Token 有效期为 3 个月,需要定期通过 Refresh Token 进行更新。
  10. *
  11. * ============ 使用示例 ============
  12. *
  13. * 1. 生成授权URL:
  14. * $auth = new Auth();
  15. * $redirectUrl = 'http://your-domain.com/callback';
  16. * $authUrl = $auth->generateAuthUrl($redirectUrl);
  17. * // 重定向到 $authUrl 让用户登录授权
  18. *
  19. * 2. 在回调页面(redirectUrl)获取 AccessToken:
  20. * $auth = new Auth();
  21. * $code = Yii::$app->request->get('code');
  22. * $result = $auth->getAccessToken($code);
  23. *
  24. * if ($result['success']) {
  25. * // 保存 access_token 和 refresh_token
  26. * // $result['access_token'] - 访问令牌
  27. * // $result['refresh_token'] - 刷新令牌
  28. * // $result['expires_in'] - 过期秒数(3个月)
  29. * }
  30. *
  31. * 3. 刷新过期的 AccessToken:
  32. * $auth = new Auth();
  33. * $result = $auth->refreshAccessToken($refreshToken);
  34. *
  35. * if ($result['success']) {
  36. * // 使用新的 access_token
  37. * // $result['access_token']
  38. * // $result['refresh_token']
  39. * // $result['expires_in']
  40. * }
  41. *
  42. * ============ 授权流程 ============
  43. *
  44. * 授权流程:
  45. * 1. 用户点击「货拉拉授权」按钮 -> generateAuthUrl() 获取授权URL
  46. * 2. 用户被重定向到货拉拉授权页面
  47. * 3. 用户填写货拉拉账号进行授权
  48. * 4. 货拉拉重定向回 redirectUrl,并携带 code
  49. * 5. 在回调页面用 code 调用 getAccessToken() 获取 token
  50. * 6. 保存 access_token 和 refresh_token 到数据库
  51. *
  52. * ============ 注意事项 ============
  53. *
  54. * 1. AccessToken 有效期为 3 个月(7776000秒)
  55. * 2. 授权码(code)有效期为 1 分钟,且只能使用 1 次
  56. * 3. 刷新令牌(refresh_token)需要妥善保管,长期有效
  57. * 4. 建议在 Access Token 过期前主动刷新
  58. * 5. 环境配置通过 YII_ENV 环境变量自动区分(production 或其他)
  59. * 6. 所有请求都是通过 GET 方式进行
  60. *
  61. * @package common\components\delivery\platform\huolala
  62. */
  63. class Auth
  64. {
  65. // 固定参数
  66. const RESPONSE_TYPE = 'code';
  67. const GRANT_TYPE_AUTH_CODE = 'authorization_code';
  68. const GRANT_TYPE_REFRESH = 'refresh_token';
  69. // 授权端点
  70. const AUTHORIZE_URL = 'https://open.huolala.cn/#/oauth/authorize';
  71. const TOKEN_URL = 'https://open.huolala.cn/oauth/token';
  72. protected $appKey;
  73. protected $appSecret;
  74. protected $redirectUri;
  75. protected $isSandbox;
  76. /**
  77. * 初始化授权类
  78. * 根据环境获取配置信息
  79. */
  80. public function __construct()
  81. {
  82. $isProduction = getenv('YII_ENV') == 'production';
  83. if($isProduction){
  84. // 货拉拉配置
  85. $this->appKey = 'gBhmCdWtX7hi7qnljduk0yK21XgQg8w3';
  86. $this->appSecret = 'bARxI68FFHpEXAcCQ23fxVQyxtJ9RONU';
  87. }else{
  88. // 货拉拉配置
  89. $this->appKey = '8S2uZQrWwM1mkG2Cj576PsMiNIe25UNT';
  90. $this->appSecret = 'olzor07UgmUWaagahNGQAKQKXRtvhK8j';
  91. }
  92. // 根据环境设置沙箱标志
  93. $this->isSandbox = !$isProduction;
  94. }
  95. /**
  96. * 设置重定向URI
  97. * 需要进行URLEncode编码
  98. *
  99. * @param string $redirectUri 重定向地址
  100. */
  101. public function setRedirectUri($redirectUri)
  102. {
  103. $this->redirectUri = $redirectUri;
  104. return $this;
  105. }
  106. /**
  107. * 生成授权URL
  108. * 用户需要通过浏览器访问此URL进行授权
  109. *
  110. * @param string $redirectUri 重定向地址(回调地址)
  111. * @return string 授权URL
  112. */
  113. public function generateAuthUrl($redirectUri)
  114. {
  115. $this->setRedirectUri($redirectUri);
  116. $params = [
  117. 'response_type' => self::RESPONSE_TYPE,
  118. 'client_id' => $this->appKey,
  119. 'redirect_uri' => $this->redirectUri,
  120. 'isSandbox' => $this->isSandbox ? 'true' : 'false',
  121. ];
  122. // 构建URL
  123. $queryString = http_build_query($params);
  124. return self::AUTHORIZE_URL . '?' . $queryString;
  125. }
  126. /**
  127. * 获取AccessToken
  128. * 使用授权码换取令牌
  129. *
  130. * 文档:/oauth/token (GET请求)
  131. * 入参:grant_type, client_id, code, isSandbox
  132. * 出参:access_token, refresh_token, expires_in
  133. *
  134. * @param string $code 授权码(来自授权页面重定向)
  135. * @return array 返回格式:['success' => true/false, 'access_token' => '', 'refresh_token' => '', 'expires_in' => 0, 'error' => '']
  136. */
  137. public function getAccessToken($code)
  138. {
  139. $params = [
  140. 'grant_type' => self::GRANT_TYPE_AUTH_CODE,
  141. 'client_id' => $this->appKey,
  142. 'code' => $code,
  143. 'isSandbox' => $this->isSandbox ? 'true' : 'false',
  144. ];
  145. $response = HttpClient::get(self::TOKEN_URL, $params);
  146. Yii::info("[HuoLalaAuthGetToken] Response: " . json_encode($response));
  147. return $this->parseResponse($response);
  148. }
  149. /**
  150. * 刷新AccessToken
  151. * 使用刷新令牌获取新的AccessToken
  152. *
  153. * 文档:/oauth/token (GET请求)
  154. * 入参:grant_type, client_id, refresh_token, isSandbox
  155. * 出参:access_token, refresh_token, expires_in
  156. *
  157. * @param string $refreshToken 刷新令牌
  158. * @return array 返回格式:['success' => true/false, 'access_token' => '', 'refresh_token' => '', 'expires_in' => 0, 'error' => '']
  159. */
  160. public function refreshAccessToken($refreshToken)
  161. {
  162. $params = [
  163. 'grant_type' => self::GRANT_TYPE_REFRESH,
  164. 'client_id' => $this->appKey,
  165. 'refresh_token' => $refreshToken,
  166. 'isSandbox' => $this->isSandbox ? 'true' : 'false',
  167. ];
  168. $response = HttpClient::get(self::TOKEN_URL, $params);
  169. Yii::info("[HuoLalaAuthRefreshToken] Response: " . json_encode($response));
  170. return $this->parseResponse($response);
  171. }
  172. /**
  173. * 解析API响应
  174. *
  175. * 货拉拉 API 返回格式:
  176. * {
  177. * "ret": 0,
  178. * "msg": "success",
  179. * "data": {
  180. * "auth_mobile": "17168665598",
  181. * "access_token": "a3207b6b5ad04249ad1dbf6a9824dbea",
  182. * "refresh_token": "4ccbbab0e9e443159c518d1d10741ad",
  183. * "auth_end_time": "2020-06-07 15:35:08"
  184. * }
  185. * }
  186. *
  187. * @param array $response HTTP响应
  188. * @return array 标准化的响应格式
  189. */
  190. protected function parseResponse($response)
  191. {
  192. // 检查 ret 字段(0 表示成功)
  193. if (!isset($response['ret'])) {
  194. return [
  195. 'success' => false,
  196. 'error' => $response['msg'] ?? '响应格式错误,缺少 ret 字段',
  197. ];
  198. }
  199. // 如果 ret 不等于 0,表示请求失败
  200. if ($response['ret'] != 0) {
  201. return [
  202. 'success' => false,
  203. 'error' => $response['msg'] ?? 'API返回异常',
  204. 'ret' => $response['ret'],
  205. ];
  206. }
  207. // 提取 data 字段中的数据
  208. $data = $response['data'] ?? [];
  209. // 检查是否包含必需的token信息
  210. if (empty($data['access_token'])) {
  211. return [
  212. 'success' => false,
  213. 'error' => $response['msg'] ?? '未获取到 access_token',
  214. ];
  215. }
  216. // 成功响应
  217. return [
  218. 'success' => true,
  219. 'access_token' => $data['access_token'] ?? '',
  220. 'refresh_token' => $data['refresh_token'] ?? '',
  221. 'auth_mobile' => $data['auth_mobile'] ?? '',
  222. 'auth_end_time' => $data['auth_end_time'] ?? '',
  223. ];
  224. }
  225. /**
  226. * 获取AppKey
  227. *
  228. * @return string
  229. */
  230. public function getAppKey()
  231. {
  232. return $this->appKey;
  233. }
  234. /**
  235. * 获取AppSecret
  236. *
  237. * @return string
  238. */
  239. public function getAppSecret()
  240. {
  241. return $this->appSecret;
  242. }
  243. /**
  244. * 获取是否为沙箱环境
  245. *
  246. * @return bool
  247. */
  248. public function isSandbox()
  249. {
  250. return $this->isSandbox;
  251. }
  252. }