| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360 |
- <?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 $appSecret;
- 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 = 'sswoXlqJvk7Be9GN3';
- $this->appSecret = 'mUa1uLQkybm6cGaUa4AW4krn4i5QROpD';
- }
- /**
- * 设置重定向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,
- ];
- $headers = ['Content-Type' => 'application/x-www-form-urlencoded;charset=utf-8'];
- $response = HttpClient::post($url, $data, $headers);
- 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->appSecret, 'md5', true);
- $params['sign'] = $sign;
- $headers = ['Content-Type' => 'application/x-www-form-urlencoded;charset=utf-8'];
- $response = HttpClient::post($url, $params, $headers);
- 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->appSecret, 'md5', true, 'shansong');
- $params['sign'] = $sign;
- $headers = ['Content-Type' => 'application/x-www-form-urlencoded;charset=utf-8'];
- $response = HttpClient::post($url, $params, $headers);
- 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 getAppSecret()
- {
- return $this->appSecret;
- }
- }
|