|
|
@@ -0,0 +1,363 @@
|
|
|
+<?php
|
|
|
+
|
|
|
+
|
|
|
+namespace common\components\delivery\platform\shanSong;
|
|
|
+
|
|
|
+use common\components\delivery\helpers\HttpClient;
|
|
|
+use common\components\delivery\helpers\SignHelper;
|
|
|
+use Yii;
|
|
|
+
|
|
|
+/**
|
|
|
+ * 闪送商户授权
|
|
|
+ *
|
|
|
+ * 支持两种授权方式:
|
|
|
+ * 1. 商户授权(isAllStoreAuth=true)- 授权后能为商户下所有门店发单
|
|
|
+ * 2. 门店授权(isAllStoreAuth=false)- 授权后只能为授权时选择的门店发单
|
|
|
+ *
|
|
|
+ * ============ 使用示例 ============
|
|
|
+ *
|
|
|
+ * 1. 生成商户授权URL(推荐用于首次授权):
|
|
|
+ * $auth = new Auth();
|
|
|
+ * $redirectUrl = 'http://your-domain.com/callback';
|
|
|
+ * $authUrl = $auth->generateMerchantAuthUrl($userId, $redirectUrl);
|
|
|
+ * // 重定向到 $authUrl 让用户登录授权
|
|
|
+ *
|
|
|
+ * 2. 生成门店授权URL(用于特定门店授权):
|
|
|
+ * $auth = new Auth();
|
|
|
+ * $authUrl = $auth->generateStoreAuthUrl($thirdStoreId, $userId, $redirectUrl);
|
|
|
+ * // 用户授权后会跳转到 redirectUrl 并携带 code
|
|
|
+ *
|
|
|
+ * 3. 在回调页面(redirectUrl)获取 AccessToken:
|
|
|
+ * $auth = new Auth();
|
|
|
+ * $code = Yii::$app->request->get('code');
|
|
|
+ * $shopId = Yii::$app->request->get('shopId');
|
|
|
+ * $result = $auth->getAccessToken($code);
|
|
|
+ *
|
|
|
+ * if ($result['success']) {
|
|
|
+ * // 保存 access_token 和 refresh_token
|
|
|
+ * // $result['access_token'] - 访问令牌
|
|
|
+ * // $result['refresh_token'] - 刷新令牌
|
|
|
+ * // $result['expires_in'] - 过期秒数(默认30天)
|
|
|
+ * }
|
|
|
+ *
|
|
|
+ * 4. 刷新过期的 AccessToken:
|
|
|
+ * $auth = new Auth();
|
|
|
+ * $result = $auth->refreshAccessToken($refreshToken);
|
|
|
+ *
|
|
|
+ * if ($result['success']) {
|
|
|
+ * // 使用新的 access_token
|
|
|
+ * }
|
|
|
+ *
|
|
|
+ * 5. 取消用户授权:
|
|
|
+ * $auth = new Auth();
|
|
|
+ * $result = $auth->cancelAuthorization($accessToken);
|
|
|
+ *
|
|
|
+ * if ($result['success']) {
|
|
|
+ * // 授权已取消
|
|
|
+ * }
|
|
|
+ *
|
|
|
+ * ============ 授权流程 ============
|
|
|
+ *
|
|
|
+ * 商户授权流程:
|
|
|
+ * 1. 用户点击「闪送授权」按钮 -> generateMerchantAuthUrl() 获取授权URL
|
|
|
+ * 2. 用户被重定向到闪送授权页面
|
|
|
+ * 3. 用户填写闪送账号进行授权
|
|
|
+ * 4. 闪送重定向回 redirectUrl,并携带 code 和 shopId
|
|
|
+ * 5. 在回调页面用 code 调用 getAccessToken() 获取 token
|
|
|
+ * 6. 保存 access_token 和 refresh_token 到数据库
|
|
|
+ *
|
|
|
+ * 门店授权流程:
|
|
|
+ * 与商户授权流程类似,但用户可以在闪送授权页面选择授权特定门店
|
|
|
+ * 返回结果中会包含 storeId(闪送门店ID)和 thirdStoreId(平台门店ID)
|
|
|
+ *
|
|
|
+ * ============ 注意事项 ============
|
|
|
+ *
|
|
|
+ * 1. AccessToken 有效期为 30 天(2592000秒)
|
|
|
+ * 2. 授权码(code)有效期为 1 分钟,且只能使用 1 次
|
|
|
+ * 3. 刷新令牌(refresh_token)需要妥善保管,长期有效
|
|
|
+ * 4. 签名算法为 MD5,签名参数中不能包含 null 或空字符串值
|
|
|
+ * 5. 建议定期刷新 token,在过期前 7 天开始刷新
|
|
|
+ * 6. 环境配置通过 YII_ENV 环境变量自动区分(production 或其他)
|
|
|
+ *
|
|
|
+ * @package common\components\shans
|
|
|
+ */
|
|
|
+class Auth
|
|
|
+{
|
|
|
+ // 授权方式常量
|
|
|
+ const AUTH_TYPE_ALL_STORE = true; // 商户授权
|
|
|
+ const AUTH_TYPE_SINGLE_STORE = false; // 门店授权
|
|
|
+
|
|
|
+ // 固定参数
|
|
|
+ const RESPONSE_TYPE = 'code';
|
|
|
+ const SCOPE = 'shop_open_api';
|
|
|
+
|
|
|
+ protected $baseUrl;
|
|
|
+ protected $clientId;
|
|
|
+ protected $clientSecret;
|
|
|
+ protected $redirectUri;
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 初始化授权类
|
|
|
+ * 根据环境获取配置信息
|
|
|
+ */
|
|
|
+ public function __construct()
|
|
|
+ {
|
|
|
+ $isProduction = getenv('YII_ENV') == 'production';
|
|
|
+
|
|
|
+ if ($isProduction) {
|
|
|
+ $this->baseUrl = 'https://open.ishansong.com';
|
|
|
+ } else {
|
|
|
+ $this->baseUrl = 'http://open.s.bingex.com';
|
|
|
+ }
|
|
|
+
|
|
|
+ $this->clientId = Yii::$app->params['shanAppId'] ?? '';
|
|
|
+ $this->clientSecret = Yii::$app->params['shanAppSecret'] ?? '';
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 设置重定向URI
|
|
|
+ * 需要进行URLEncode编码
|
|
|
+ *
|
|
|
+ * @param string $redirectUri 重定向地址
|
|
|
+ */
|
|
|
+ public function setRedirectUri($redirectUri)
|
|
|
+ {
|
|
|
+ $this->redirectUri = urlencode($redirectUri);
|
|
|
+ return $this;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 生成商户授权URL
|
|
|
+ * 授权后能为商户下所有门店发单
|
|
|
+ *
|
|
|
+ * @param string $state 状态参数,用于标记平台用户,建议使用用户ID
|
|
|
+ * @param string $redirectUri 重定向地址
|
|
|
+ * @return string 授权URL
|
|
|
+ */
|
|
|
+ public function generateMerchantAuthUrl($state, $redirectUri = null)
|
|
|
+ {
|
|
|
+ if ($redirectUri) {
|
|
|
+ $this->setRedirectUri($redirectUri);
|
|
|
+ }
|
|
|
+
|
|
|
+ return $this->buildAuthUrl(self::AUTH_TYPE_ALL_STORE, $state);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 生成门店授权URL
|
|
|
+ * 授权后只能为授权时选择的门店发单
|
|
|
+ *
|
|
|
+ * @param string $thirdStoreId 第三方平台的门店ID
|
|
|
+ * @param string $state 状态参数,用于标记平台用户
|
|
|
+ * @param string $redirectUri 重定向地址
|
|
|
+ * @return string 授权URL
|
|
|
+ */
|
|
|
+ public function generateStoreAuthUrl($thirdStoreId, $state, $redirectUri = null)
|
|
|
+ {
|
|
|
+ if ($redirectUri) {
|
|
|
+ $this->setRedirectUri($redirectUri);
|
|
|
+ }
|
|
|
+
|
|
|
+ return $this->buildAuthUrl(self::AUTH_TYPE_SINGLE_STORE, $state, $thirdStoreId);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 构建授权URL
|
|
|
+ *
|
|
|
+ * @param bool $isAllStoreAuth 是否为商户授权
|
|
|
+ * @param string $state 状态参数
|
|
|
+ * @param string $thirdStoreId 门店授权时的门店ID
|
|
|
+ * @return string 授权URL
|
|
|
+ */
|
|
|
+ protected function buildAuthUrl($isAllStoreAuth, $state, $thirdStoreId = null)
|
|
|
+ {
|
|
|
+ $params = [
|
|
|
+ 'isAllStoreAuth' => $isAllStoreAuth ? 'true' : 'false',
|
|
|
+ 'response_type' => self::RESPONSE_TYPE,
|
|
|
+ 'scope' => self::SCOPE,
|
|
|
+ 'state' => $state,
|
|
|
+ 'client_id' => $this->clientId,
|
|
|
+ 'redirect_uri' => $this->redirectUri,
|
|
|
+ ];
|
|
|
+
|
|
|
+ // 门店授权需要添加thirdStoreId
|
|
|
+ if ($thirdStoreId !== null && !$isAllStoreAuth) {
|
|
|
+ $params['thirdStoreId'] = $thirdStoreId;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 构建URL
|
|
|
+ $queryString = http_build_query($params);
|
|
|
+ return $this->baseUrl . '/auth?' . $queryString;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取AccessToken
|
|
|
+ * 使用授权码换取令牌
|
|
|
+ *
|
|
|
+ * 文档:/openapi/oauth/token
|
|
|
+ * 入参:clientId, code
|
|
|
+ * 出参:access_token, refresh_token, expires_in
|
|
|
+ *
|
|
|
+ * @param string $code 授权码(来自授权页面重定向)
|
|
|
+ * @return array 返回格式:['access_token' => '', 'refresh_token' => '', 'expires_in' => 0, 'error' => '']
|
|
|
+ */
|
|
|
+ public function getAccessToken($code)
|
|
|
+ {
|
|
|
+ $url = $this->baseUrl . '/openapi/oauth/token';
|
|
|
+
|
|
|
+ $data = [
|
|
|
+ 'clientId' => $this->clientId,
|
|
|
+ 'code' => $code,
|
|
|
+ ];
|
|
|
+
|
|
|
+ $response = HttpClient::post($url, $data);
|
|
|
+
|
|
|
+ return $this->parseResponse($response);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 刷新AccessToken
|
|
|
+ * 使用刷新令牌获取新的AccessToken
|
|
|
+ *
|
|
|
+ * 文档:/openapi/oauth/refresh_token
|
|
|
+ * 入参:clientId, sign, timestamp, data
|
|
|
+ * 出参:access_token, expires_in
|
|
|
+ *
|
|
|
+ * @param string $refreshToken 刷新令牌
|
|
|
+ * @return array 返回格式:['access_token' => '', 'expires_in' => 0, 'error' => '']
|
|
|
+ */
|
|
|
+ public function refreshAccessToken($refreshToken)
|
|
|
+ {
|
|
|
+ $url = $this->baseUrl . '/openapi/oauth/refresh_token';
|
|
|
+
|
|
|
+ $timestamp = (string) (int) (microtime(true) * 1000);
|
|
|
+ $data = json_encode(['refreshToken' => $refreshToken], JSON_UNESCAPED_UNICODE);
|
|
|
+
|
|
|
+ $params = [
|
|
|
+ 'clientId' => $this->clientId,
|
|
|
+ 'timestamp' => $timestamp,
|
|
|
+ 'data' => $data,
|
|
|
+ ];
|
|
|
+
|
|
|
+ // 计算签名
|
|
|
+ $sign = SignHelper::makeSign($params, $this->clientSecret, 'md5', true);
|
|
|
+ $params['sign'] = $sign;
|
|
|
+
|
|
|
+ $response = HttpClient::post($url, $params);
|
|
|
+
|
|
|
+ return $this->parseResponse($response);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 取消授权
|
|
|
+ * 撤销用户的授权,使其accessToken失效
|
|
|
+ *
|
|
|
+ * 文档:/openapi/oauth/cancel
|
|
|
+ * 入参:clientId, sign, timestamp, data
|
|
|
+ * 出参:无数据返回
|
|
|
+ *
|
|
|
+ * @param string $accessToken 待取消的授权令牌
|
|
|
+ * @return array 返回格式:['success' => true/false, 'message' => '', 'error' => '']
|
|
|
+ */
|
|
|
+ public function cancelAuthorization($accessToken)
|
|
|
+ {
|
|
|
+ $url = $this->baseUrl . '/openapi/oauth/cancel';
|
|
|
+
|
|
|
+ $timestamp = (string) (int) (microtime(true) * 1000);
|
|
|
+ $data = json_encode(['accessToken' => $accessToken], JSON_UNESCAPED_UNICODE);
|
|
|
+
|
|
|
+ $params = [
|
|
|
+ 'clientId' => $this->clientId,
|
|
|
+ 'timestamp' => $timestamp,
|
|
|
+ 'data' => $data,
|
|
|
+ ];
|
|
|
+
|
|
|
+ // 计算签名
|
|
|
+ $sign = SignHelper::makeSign($params, $this->clientSecret, 'md5', true);
|
|
|
+ $params['sign'] = $sign;
|
|
|
+
|
|
|
+ $response = HttpClient::post($url, $params);
|
|
|
+
|
|
|
+ Yii::info("[ShansAuthCancel] Response: " . json_encode($response));
|
|
|
+
|
|
|
+ if (isset($response['status']) && $response['status'] == 200) {
|
|
|
+ return [
|
|
|
+ 'success' => true,
|
|
|
+ 'message' => $response['msg'] ?? '取消授权成功',
|
|
|
+ ];
|
|
|
+ }
|
|
|
+
|
|
|
+ return [
|
|
|
+ 'success' => false,
|
|
|
+ 'message' => $response['msg'] ?? '取消授权失败',
|
|
|
+ 'error' => $response['error'] ?? '',
|
|
|
+ ];
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 解析API响应
|
|
|
+ *
|
|
|
+ * @param array $response HTTP响应
|
|
|
+ * @return array 标准化的响应格式
|
|
|
+ */
|
|
|
+ protected function parseResponse($response)
|
|
|
+ {
|
|
|
+ // 如果是HTTP错误
|
|
|
+ if (isset($response['code']) && $response['code'] != 200) {
|
|
|
+ return [
|
|
|
+ 'success' => false,
|
|
|
+ 'error' => $response['error'] ?? '请求失败',
|
|
|
+ 'status_code' => $response['code'] ?? 0,
|
|
|
+ ];
|
|
|
+ }
|
|
|
+
|
|
|
+ // 检查API状态码
|
|
|
+ if (isset($response['status']) && $response['status'] != 200) {
|
|
|
+ return [
|
|
|
+ 'success' => false,
|
|
|
+ 'error' => $response['msg'] ?? 'API返回异常',
|
|
|
+ 'status' => $response['status'],
|
|
|
+ ];
|
|
|
+ }
|
|
|
+
|
|
|
+ // 成功响应
|
|
|
+ $data = $response['data'] ?? [];
|
|
|
+
|
|
|
+ return [
|
|
|
+ 'success' => true,
|
|
|
+ 'access_token' => $data['access_token'] ?? '',
|
|
|
+ 'refresh_token' => $data['refresh_token'] ?? '',
|
|
|
+ 'expires_in' => $data['expires_in'] ?? 0,
|
|
|
+ ];
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取基础URL
|
|
|
+ *
|
|
|
+ * @return string
|
|
|
+ */
|
|
|
+ public function getBaseUrl()
|
|
|
+ {
|
|
|
+ return $this->baseUrl;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取客户端ID
|
|
|
+ *
|
|
|
+ * @return string
|
|
|
+ */
|
|
|
+ public function getClientId()
|
|
|
+ {
|
|
|
+ return $this->clientId;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取客户端密钥
|
|
|
+ *
|
|
|
+ * @return string
|
|
|
+ */
|
|
|
+ public function getClientSecret()
|
|
|
+ {
|
|
|
+ return $this->clientSecret;
|
|
|
+ }
|
|
|
+}
|