AlibabaAgentBaseService.php 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450
  1. <?php
  2. /**
  3. * 1688分销严选采购解决方案(分销买家版) - 基础服务类
  4. *
  5. * 提供OAuth 2.0认证、API请求签名、HTTP请求等基础能力
  6. *
  7. * @author: yourname
  8. * @day: 2026/04/27
  9. */
  10. namespace app\services\ThirdParty\AlibabaAgent;
  11. use crmeb\services\HttpService;
  12. use think\facade\Config;
  13. use think\facade\Log;
  14. class AlibabaAgentBaseService
  15. {
  16. /** @var array 配置 */
  17. protected $config;
  18. /** @var string App Key */
  19. protected $appKey;
  20. /** @var string App Secret */
  21. protected $appSecret;
  22. /** @var string 网关地址 */
  23. protected $gatewayUrl;
  24. /** @var string 访问令牌 */
  25. protected $accessToken = '';
  26. /** @var string 刷新令牌 */
  27. protected $refreshToken = '';
  28. /**
  29. * 构造函数
  30. */
  31. public function __construct()
  32. {
  33. $this->config = Config::get('third_party.alibaba_agent', []);
  34. $this->appKey = $this->config['appKey'] ?? '';
  35. $this->appSecret = $this->config['appSecret'] ?? '';
  36. $this->accessToken = 'ce15fc70-b462-43bf-acc3-f87e43c858ba';
  37. $this->gatewayUrl = $this->config['gatewayUrl'] ?? 'https://gw.open.1688.com/openapi/';
  38. }
  39. /**
  40. * 设置访问令牌
  41. *
  42. * @param string $accessToken
  43. * @return $this
  44. */
  45. public function setAccessToken(string $accessToken): self
  46. {
  47. $this->accessToken = $accessToken;
  48. return $this;
  49. }
  50. /**
  51. * 设置刷新令牌
  52. *
  53. * @param string $refreshToken
  54. * @return $this
  55. */
  56. public function setRefreshToken(string $refreshToken): self
  57. {
  58. $this->refreshToken = $refreshToken;
  59. return $this;
  60. }
  61. /**
  62. * 获取访问令牌
  63. *
  64. * @return string
  65. */
  66. public function getAccessToken(): string
  67. {
  68. return $this->accessToken;
  69. }
  70. /**
  71. * 获取OAuth 2.0授权URL
  72. *
  73. * @param string $state 防CSRF状态码
  74. * @return string
  75. */
  76. public function getAuthUrl(string $state = ''): string
  77. {
  78. $params = [
  79. 'client_id' => $this->appKey,
  80. 'redirect_uri' => $this->config['redirectUri'] ?? '',
  81. 'response_type' => 'code',
  82. 'state' => $state,
  83. ];
  84. return 'https://auth.1688.com/oauth/authorize?' . http_build_query($params);
  85. }
  86. /**
  87. * 通过授权码获取访问令牌
  88. *
  89. * @param string $code 授权码
  90. * @return array|false
  91. */
  92. public function getAccessTokenByCode(string $code)
  93. {
  94. $url = 'https://gw.open.1688.com/openapi/http/1/system.oauth2/getAccessTokenByCode/' . $this->appKey;
  95. $params = [
  96. 'code' => $code,
  97. 'appKey' => $this->appKey,
  98. 'appSecret' => $this->appSecret,
  99. 'redirect_uri' => $this->config['redirectUri'] ?? '',
  100. 'grant_type' => 'authorization_code',
  101. ];
  102. return $this->parseResponse(
  103. HttpService::postRequest($url, $params)
  104. );
  105. }
  106. /**
  107. * 刷新访问令牌
  108. *
  109. * @param string $refreshToken
  110. * @return array|false
  111. */
  112. public function refreshAccessToken(string $refreshToken)
  113. {
  114. $url = 'https://gw.open.1688.com/openapi/http/1/system.oauth2/getAccessTokenByCode/' . $this->appKey;
  115. $params = [
  116. 'refreshToken' => $refreshToken,
  117. 'appKey' => $this->appKey,
  118. 'appSecret' => $this->appSecret,
  119. 'grant_type' => 'refresh_token',
  120. ];
  121. $result = $this->parseResponse(
  122. HttpService::postRequest($url, $params)
  123. );
  124. if ($result && isset($result['access_token'])) {
  125. $this->setAccessToken($result['access_token']);
  126. if (isset($result['refresh_token'])) {
  127. $this->setRefreshToken($result['refresh_token']);
  128. }
  129. }
  130. return $result;
  131. }
  132. /**
  133. * 生成1688 API签名(param2方式)
  134. *
  135. * 参考可调通的代码:
  136. * $aliParams = array();
  137. * foreach ($code_arr as $key => $val) {
  138. * if (is_array($val)) { $val = json_encode($val); }
  139. * $aliParams[] = $key . $val;
  140. * }
  141. * sort($aliParams);
  142. * $sign_str = join('', $aliParams);
  143. * $sign_str = $param . $apiInfo . $sign_str;
  144. * return strtoupper(bin2hex(hash_hmac("sha1", $sign_str, $appSecret, true)));
  145. *
  146. * @param string $param 协议前缀,如 "param2/1/"
  147. * @param string $apiInfo API路径(含appKey),如 "com.alibaba.fenxiao/jxhy.product.getPageList/6512262"
  148. * @param array $params 所有请求参数(含access_token,不含_aop_signature)
  149. * @return string
  150. */
  151. protected function generateSignature(string $param, string $apiInfo, array $params): string
  152. {
  153. $aliParams = array();
  154. foreach ($params as $key => $val) {
  155. // 数组参数需要json_encode
  156. if (is_array($val)) {
  157. $val = json_encode($val);
  158. }
  159. $aliParams[] = $key . $val;
  160. }
  161. sort($aliParams);
  162. $sign_str = join('', $aliParams);
  163. // 注意顺序: param + apiInfo + 参数拼装
  164. $sign_str = $param . $apiInfo . $sign_str;
  165. return strtoupper(bin2hex(hash_hmac("sha1", $sign_str, $this->appSecret, true)));
  166. }
  167. /**
  168. * 执行API请求(param2方式 - RESTful风格)
  169. *
  170. * @param string $namespace API命名空间
  171. * @param string $name API名称
  172. * @param int $version API版本号
  173. * @param array $businessParams 业务参数
  174. * @return array|false
  175. */
  176. protected function executeParam2(
  177. string $namespace,
  178. string $name,
  179. int $version = 1,
  180. array $businessParams = []
  181. ) {
  182. // param2方式URL格式: https://gw.open.1688.com/openapi/param2/{version}/{namespace}/{name}/{appKey}
  183. // 注意: URL路径最后一部分是 appKey,不是API版本号
  184. $appKey = $this->getConfig('appKey', '');
  185. // 协议前缀: param2/{version}/
  186. $param = 'param2/' . $version . '/';
  187. // API路径(含appKey): {namespace}/{name}/{appKey}
  188. $apiInfo = $namespace . '/' . $name . '/' . $appKey;
  189. // 完整URL(不含query参数,参数通过POST body发送)
  190. $url = rtrim($this->gatewayUrl, '/') . '/' . $param . $apiInfo;
  191. // 所有参数(包括access_token和业务参数)
  192. $allParams = array_merge(
  193. ['access_token' => $this->accessToken],
  194. $businessParams
  195. );
  196. // 生成签名
  197. // 注意: generateSignature($param, $apiInfo, $params)
  198. // sign_str = $param . $apiInfo . 参数拼装
  199. $signature = $this->generateSignature($param, $apiInfo, $allParams);
  200. $allParams['_aop_signature'] = $signature;
  201. $this->logRequest($url, $allParams);
  202. try {
  203. // 使用POST方式发送请求,参数放在POST body中(参考可调通的代码)
  204. $ch = curl_init();
  205. curl_setopt($ch, CURLOPT_URL, $url);
  206. curl_setopt($ch, CURLOPT_POST, true);
  207. curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($allParams));
  208. curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
  209. curl_setopt($ch, CURLOPT_TIMEOUT, 15);
  210. curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
  211. curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
  212. curl_setopt($ch, CURLOPT_FAILONERROR, false);
  213. $response = curl_exec($ch);
  214. $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
  215. $curlError = curl_error($ch);
  216. curl_close($ch);
  217. if ($response === false) {
  218. $this->logError('HTTP请求失败', $curlError ?: '未知curl错误');
  219. return false;
  220. }
  221. $this->logResponse($url, $response);
  222. return $this->parseResponse($response, $httpCode);
  223. } catch (\Exception $e) {
  224. $this->logError('API请求异常', $e->getMessage());
  225. return false;
  226. }
  227. }
  228. /**
  229. * 执行API请求(http方式 - 传统POST方式)
  230. *
  231. * @param string $namespace API命名空间
  232. * @param string $name API名称
  233. * @param int $version API版本号
  234. * @param array $businessParams 业务参数
  235. * @return array|false
  236. */
  237. protected function executeHttp(
  238. string $namespace,
  239. string $name,
  240. int $version = 1,
  241. array $businessParams = []
  242. ) {
  243. // 构建URL: https://gw.open.1688.com/openapi/http/1/cn.alibaba.open/offer.get/1
  244. $url = rtrim($this->gatewayUrl, '/') . '/http/' . $version . '/' . $namespace . '/' . $name . '/' . $version;
  245. // 构建请求参数
  246. $params = [
  247. 'access_token' => $this->accessToken,
  248. 'appKey' => $this->appKey,
  249. 'appSecret' => $this->appSecret,
  250. ];
  251. // 业务参数
  252. if (!empty($businessParams)) {
  253. $params = array_merge($params, $businessParams);
  254. }
  255. $this->logRequest($url, $params);
  256. try {
  257. $response = HttpService::postRequest($url, $params);
  258. $this->logResponse($url, $response);
  259. return $this->parseResponse($response);
  260. } catch (\Exception $e) {
  261. $this->logError('API请求异常', $e->getMessage());
  262. return false;
  263. }
  264. }
  265. /**
  266. * 解析响应
  267. *
  268. * @param string|false $response 响应体
  269. * @param int $httpCode HTTP状态码
  270. * @return array|false
  271. */
  272. protected function parseResponse($response, int $httpCode = 200)
  273. {
  274. if ($response === false || empty($response)) {
  275. $this->logError('HTTP请求失败', '响应为空或请求失败');
  276. return false;
  277. }
  278. $result = json_decode($response, true);
  279. if (json_last_error() !== JSON_ERROR_NONE) {
  280. $this->logError('JSON解析失败', json_last_error_msg() . ' | 原始响应: ' . substr($response, 0, 500));
  281. return false;
  282. }
  283. // HTTP状态码非200,记录错误并返回false
  284. if ($httpCode !== 200) {
  285. $errorMsg = json_encode($result, JSON_UNESCAPED_UNICODE);
  286. $this->logError("HTTP请求失败(status:{$httpCode})", $errorMsg);
  287. return false;
  288. }
  289. // 检查1688 API标准错误格式
  290. if (isset($result['error_code']) || isset($result['errorMessage'])) {
  291. $errorMsg = $result['errorMessage'] ?? ($result['error_message'] ?? '未知错误');
  292. $errorCode = $result['error_code'] ?? ($result['code'] ?? 0);
  293. $this->logError('1688 API错误', "[{$errorCode}] {$errorMsg}");
  294. return false;
  295. }
  296. // 检查result中的错误
  297. if (isset($result['result']) && isset($result['result']['errorCode'])) {
  298. $errorMsg = $result['result']['errorMessage'] ?? '未知错误';
  299. $errorCode = $result['result']['errorCode'];
  300. $this->logError('1688 API业务错误', "[{$errorCode}] {$errorMsg}");
  301. return false;
  302. }
  303. // 检查alibaba标准错误响应格式
  304. if (isset($result['errorResponse']) && isset($result['errorResponse']['code'])) {
  305. $errorMsg = $result['errorResponse']['message'] ?? '未知错误';
  306. $errorCode = $result['errorResponse']['code'];
  307. $this->logError('1688 API错误响应', "[{$errorCode}] {$errorMsg}");
  308. return false;
  309. }
  310. return $result;
  311. }
  312. /**
  313. * 记录请求日志
  314. *
  315. * @param string $url
  316. * @param array $params
  317. */
  318. protected function logRequest(string $url, array $params): void
  319. {
  320. if (!($this->config['logEnabled'] ?? false)) {
  321. return;
  322. }
  323. $logData = [
  324. 'url' => $url,
  325. 'params' => $this->maskSensitiveData($params),
  326. ];
  327. Log::channel('alibaba')->info('[1688分销采购] 请求: ' . json_encode($logData, JSON_UNESCAPED_UNICODE));
  328. }
  329. /**
  330. * 记录响应日志
  331. *
  332. * @param string $url
  333. * @param string|false $response
  334. */
  335. protected function logResponse(string $url, $response): void
  336. {
  337. if (!($this->config['logEnabled'] ?? false)) {
  338. return;
  339. }
  340. Log::channel('alibaba')->info('[1688分销采购] 响应: ' . substr((string)$response, 0, 2000));
  341. }
  342. /**
  343. * 记录错误日志
  344. *
  345. * @param string $title
  346. * @param string $message
  347. */
  348. protected function logError(string $title, string $message): void
  349. {
  350. Log::channel('alibaba')->error("[1688分销采购] {$title}: {$message}");
  351. // 同时写入文件日志(兼容旧的文件日志方式)
  352. $logPath = $this->config['logPath'] ?? runtime_path() . 'alibaba/';
  353. $logFile = $logPath . 'error_' . date('Y-m-d') . '.log';
  354. $logDir = dirname($logFile);
  355. if (!is_dir($logDir)) {
  356. @mkdir($logDir, 0755, true);
  357. }
  358. @file_put_contents(
  359. $logFile,
  360. '[' . date('Y-m-d H:i:s') . "] {$title}: {$message}\n",
  361. FILE_APPEND
  362. );
  363. }
  364. /**
  365. * 脱敏敏感数据
  366. *
  367. * @param array $data
  368. * @return array
  369. */
  370. protected function maskSensitiveData(array $data): array
  371. {
  372. $sensitiveKeys = ['appSecret', 'access_token', 'refreshToken'];
  373. foreach ($data as $key => $value) {
  374. if (in_array($key, $sensitiveKeys) && is_string($value)) {
  375. $data[$key] = substr($value, 0, 4) . '****' . substr($value, -4);
  376. }
  377. }
  378. return $data;
  379. }
  380. /**
  381. * 获取配置项
  382. *
  383. * @param string $key
  384. * @param mixed $default
  385. * @return mixed
  386. */
  387. protected function getConfig(string $key, $default = null)
  388. {
  389. return $this->config[$key] ?? $default;
  390. }
  391. }