Kaynağa Gözat

feat(admin): 集成1688分销严选商品功能

- 添加1688分销严选商品相关路由配置
- 新增AlibabaAgentBaseService基础服务类,提供OAuth 2.0认证和API请求功能
- 实现ProductService商品服务类,支持商品搜索和详情查询
- 添加AdminController商品控制器,提供商品列表和详情接口
- 配置1688分销严选相关第三方配置参数
- 实现1688 API签名算法和参数格式化处理逻辑
- 添加完整的错误处理和日志记录机制
shichen 3 ay önce
ebeveyn
işleme
cc1b065830

+ 110 - 0
app/controller/admin/alibaba/Goods.php

@@ -0,0 +1,110 @@
1
+<?php
2
+
3
+namespace app\controller\admin\alibaba;
4
+
5
+use app\services\ThirdParty\AlibabaAgent\ProductService;
6
+
7
+/**
8
+ * 1688分销严选 - 商品管理
9
+ *
10
+ * API文档:
11
+ *   商品列表: https://open.1688.com/api/apidocdetail.htm?id=com.alibaba.fenxiao%3Ajxhy.product.getPageList-1
12
+ *   商品详情: https://open.1688.com/api/apidocdetail.htm?id=com.alibaba.fenxiao%3Aalibaba.fenxiao.productInfo.get-1
13
+ */
14
+class Goods
15
+{
16
+    /**
17
+     * 获取1688分销商品分页列表
18
+     *
19
+     * @param ProductService $productService
20
+     * @return \think\response\Json
21
+     *
22
+     * @api {GET} /admin/alibaba/goods/lst 获取商品列表
23
+     * @apiParam {string}  [access_token]  1688 access_token(不传则使用配置中的默认token)
24
+     * @apiParam {int}     [page=1]        页码
25
+     * @apiParam {int}     [limit=20]      每页条数(最大50)
26
+     * @apiParam {string}  [keyword]       关键词搜索
27
+     * @apiParam {int}     [categoryId]    类目ID
28
+     * @apiParam {number}  [minPrice]      最低价格
29
+     * @apiParam {number}  [maxPrice]      最高价格
30
+     * @apiParam {string}  [sort]          排序方式
31
+     */
32
+    public function lst(ProductService $productService)
33
+    {
34
+        $request = app('request');
35
+
36
+        // 获取请求参数
37
+        $page = (int)$request->param('page', 1);
38
+        $limit = (int)$request->param('limit', 20);
39
+        $keyword = $request->param('keyword', '');
40
+        $categoryId = $request->param('categoryId', '');
41
+        $minPrice = $request->param('minPrice', '');
42
+        $maxPrice = $request->param('maxPrice', '');
43
+        $sort = $request->param('sort', '');
44
+
45
+        // 构建搜索参数
46
+        $params = [
47
+            'page' => max(1, $page),
48
+            'limit' => min(max(1, $limit), 50),
49
+        ];
50
+
51
+        if (!empty($keyword)) {
52
+            $params['keyword'] = $keyword;
53
+        }
54
+
55
+        if (!empty($categoryId)) {
56
+            $params['categoryId'] = (int)$categoryId;
57
+        }
58
+
59
+        if ($minPrice !== '' && $minPrice > 0) {
60
+            $params['minPrice'] = (float)$minPrice;
61
+        }
62
+
63
+        if ($maxPrice !== '' && $maxPrice > 0) {
64
+            $params['maxPrice'] = (float)$maxPrice;
65
+        }
66
+
67
+        if (!empty($sort)) {
68
+            $params['sort'] = $sort;
69
+        }
70
+
71
+        // 调用1688 API获取商品列表
72
+        $result = $productService->searchProducts($params);
73
+
74
+        if ($result === false) {
75
+            return app('json')->fail('获取1688商品列表失败,请查看日志');
76
+        }
77
+
78
+        return app('json')->success($result);
79
+    }
80
+
81
+    /**
82
+     * 获取1688分销商品详情
83
+     *
84
+     * @param ProductService $productService
85
+     * @return \think\response\Json
86
+     *
87
+     * @api {GET} /admin/alibaba/goods/detail 获取商品详情
88
+     * @apiParam {string}  [access_token]  1688 access_token(不传则使用配置中的默认token)
89
+     * @apiParam {int}     productId       1688商品ID(必填)
90
+     */
91
+    public function detail(ProductService $productService)
92
+    {
93
+        $request = app('request');
94
+
95
+        $productId = $request->param('productId', '');
96
+
97
+        if (empty($productId)) {
98
+            return app('json')->fail('参数错误:productId不能为空');
99
+        }
100
+
101
+        // 调用1688 API获取商品详情
102
+        $result = $productService->getProductDetail($productId);
103
+
104
+        if ($result === false) {
105
+            return app('json')->fail('获取1688商品详情失败,请查看日志');
106
+        }
107
+
108
+        return app('json')->success($result);
109
+    }
110
+}

+ 449 - 0
app/services/ThirdParty/AlibabaAgent/AlibabaAgentBaseService.php

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

+ 349 - 0
app/services/ThirdParty/AlibabaAgent/ProductService.php

@@ -0,0 +1,349 @@
1
+<?php
2
+/**
3
+ * 1688分销严选采购解决方案 - 商品服务类
4
+ *
5
+ * 提供商品搜索/列表查询功能
6
+ * API文档: https://open.1688.com/api/apidocdetail.htm?id=com.alibaba.fenxiao%3Ajxhy.product.getPageList-1&aopApiCategory=category_new
7
+ *
8
+ * @author: yourname
9
+ * @day: 2026/04/27
10
+ */
11
+
12
+namespace app\services\ThirdParty\AlibabaAgent;
13
+
14
+class ProductService extends AlibabaAgentBaseService
15
+{
16
+    /**
17
+     * 获取分销商品分页列表
18
+     *
19
+     * API: com.alibaba.fenxiao:jxhy.product.getPageList-1
20
+     *
21
+     * @param array $params 搜索参数
22
+     * @return array|false
23
+     */
24
+    public function searchProducts(array $params = [])
25
+    {
26
+        $businessParams = [];
27
+
28
+        // 关键词搜索
29
+        if (!empty($params['keyword'])) {
30
+            $businessParams['keyword'] = $params['keyword'];
31
+        }
32
+
33
+        // 类目ID
34
+        if (!empty($params['categoryId'])) {
35
+            $businessParams['categoryId'] = $params['categoryId'];
36
+        }
37
+
38
+        // 页码,默认1
39
+        $businessParams['pageNo'] = $params['page'] ?? 1;
40
+
41
+        // 每页条数,最大50
42
+        $businessParams['pageSize'] = min($params['limit'] ?? 20, 50);
43
+
44
+        // 排序方式
45
+        if (!empty($params['sort'])) {
46
+            $businessParams['sort'] = $params['sort'];
47
+        }
48
+
49
+        // 最低价格过滤(仅当 > 0 时传入)
50
+        if (!empty($params['minPrice']) && $params['minPrice'] > 0) {
51
+            $businessParams['priceStart'] = $params['minPrice'];
52
+        }
53
+
54
+        // 最高价格过滤(仅当 > 0 时传入)
55
+        if (!empty($params['maxPrice']) && $params['maxPrice'] > 0) {
56
+            $businessParams['priceEnd'] = $params['maxPrice'];
57
+        }
58
+
59
+        $result = $this->executeParam2(
60
+            'com.alibaba.fenxiao',
61
+            'jxhy.product.getPageList',
62
+            1,
63
+            $businessParams
64
+        );
65
+
66
+        if ($result === false) {
67
+            return false;
68
+        }
69
+
70
+        return $this->formatSearchResult($result);
71
+    }
72
+
73
+    /**
74
+     * 格式化搜索结果
75
+     *
76
+     * @param array $result
77
+     * @return array
78
+     */
79
+    protected function formatSearchResult(array $result): array
80
+    {
81
+        $productList = [];
82
+
83
+        // 1688 API返回结构:
84
+        // {
85
+        //   "result": {
86
+        //     "success": true,
87
+        //     "code": "0",
88
+        //     "result": [ {商品1}, {商品2}, ... ],  // 商品列表
89
+        //     "pageInfo": {
90
+        //       "currentPage": 1,
91
+        //       "totalRecords": 2000,
92
+        //       "pageSize": 10
93
+        //     }
94
+        //   }
95
+        // }
96
+        $data = $result['result'] ?? $result;
97
+        $products = $data['result'] ?? [];  // 商品列表在 result.result 中
98
+        $pageInfo = $data['pageInfo'] ?? [];
99
+
100
+        if (empty($products)) {
101
+            return [
102
+                'total' => 0,
103
+                'page' => (int)($pageInfo['currentPage'] ?? 1),
104
+                'pageSize' => (int)($pageInfo['pageSize'] ?? 20),
105
+                'list' => [],
106
+            ];
107
+        }
108
+
109
+        foreach ($products as $product) {
110
+            $productList[] = $this->formatProduct($product);
111
+        }
112
+
113
+        return [
114
+            'total' => (int)($pageInfo['totalRecords'] ?? count($productList)),
115
+            'page' => (int)($pageInfo['currentPage'] ?? 1),
116
+            'pageSize' => (int)($pageInfo['pageSize'] ?? 20),
117
+            'list' => $productList,
118
+        ];
119
+    }
120
+
121
+    /**
122
+     * 格式化商品数据为统一格式
123
+     *
124
+     * @param array $product
125
+     * @return array
126
+     */
127
+    public function formatProduct(array $product): array
128
+    {
129
+        // 1688分销商品列表API返回字段:
130
+        // itemId, title, imgUrl, minPrice, maxPrice, salesCnt90d, skuCnt, serviceList
131
+
132
+        // 主图
133
+        $mainImage = $product['imgUrl'] ?? $product['mainImage'] ?? $product['mainPicture'] ?? '';
134
+
135
+        // 服务标签
136
+        $serviceTags = [];
137
+        if (!empty($product['serviceList'])) {
138
+            foreach ($product['serviceList'] as $service) {
139
+                $serviceTags[] = $service['name'] ?? '';
140
+            }
141
+        }
142
+
143
+        return [
144
+            // 基础信息
145
+            'productId' => $product['itemId'] ?? $product['productId'] ?? $product['offerId'] ?? $product['id'] ?? 0,
146
+            'productType' => $product['productType'] ?? '',
147
+            'categoryId' => $product['categoryId'] ?? $product['catId'] ?? 0,
148
+            'categoryName' => $product['categoryName'] ?? '',
149
+
150
+            // 标题与描述
151
+            'title' => $product['title'] ?? $product['subject'] ?? $product['name'] ?? '',
152
+            'description' => $product['description'] ?? $product['detail'] ?? '',
153
+
154
+            // 价格信息
155
+            'price' => $product['minPrice'] ?? $product['price'] ?? $product['offerPrice'] ?? 0,
156
+            'salePrice' => $product['maxPrice'] ?? $product['salePrice'] ?? $product['price'] ?? 0,
157
+            'minPrice' => $product['minPrice'] ?? $product['price'] ?? 0,
158
+            'maxPrice' => $product['maxPrice'] ?? $product['price'] ?? 0,
159
+
160
+            // 库存信息(该接口不返回库存,用skuCnt作为参考)
161
+            'stock' => $product['stock'] ?? $product['totalAvailableStock'] ?? 0,
162
+            'unit' => $product['unit'] ?? '件',
163
+
164
+            // 图片信息
165
+            'mainImage' => $mainImage,
166
+            'imageList' => [$mainImage],
167
+
168
+            // SKU数量
169
+            'skuCnt' => $product['skuCnt'] ?? 0,
170
+
171
+            // 销售信息
172
+            'monthSales' => $product['monthSales'] ?? $product['salesCnt90d'] ?? 0,
173
+            'totalSales' => $product['totalSales'] ?? 0,
174
+
175
+            // 服务标签
176
+            'serviceTags' => $serviceTags,
177
+
178
+            // 分销属性
179
+            'isDistribution' => $product['isDistribution'] ?? false,
180
+            'isStrictSelected' => $product['isStrictSelected'] ?? false,
181
+            'distributorPrice' => $product['distributorPrice'] ?? 0,
182
+
183
+            // 店铺信息(该接口不返回店铺信息)
184
+            'shopName' => $product['shopName'] ?? $product['companyName'] ?? '',
185
+            'shopId' => $product['shopId'] ?? $product['memberId'] ?? '',
186
+
187
+            // 链接
188
+            'detailUrl' => $product['detailUrl'] ?? $product['offerUrl'] ?? '',
189
+        ];
190
+    }
191
+
192
+    /**
193
+     * 获取分销商品详情(含SKU、图片等完整信息)
194
+     *
195
+     * API: com.alibaba.fenxiao:alibaba.pifatuan.product.detail.list-2
196
+     * 文档: https://open.1688.com/api/apidocdetail.htm?id=com.alibaba.fenxiao%3Aalibaba.pifatuan.product.detail.list-2
197
+     *
198
+     * @param int|string $productId 1688商品ID
199
+     * @return array|false
200
+     */
201
+    public function getProductDetail($productId)
202
+    {
203
+        if (empty($productId)) {
204
+            $this->logError('参数错误', 'productId不能为空');
205
+            return false;
206
+        }
207
+
208
+        // API要求参数名为 offerIds,类型为 Long[](数组)
209
+        $businessParams = [
210
+            'offerIds' => '[' . $productId . ']',
211
+        ];
212
+
213
+        $result = $this->executeParam2(
214
+            'com.alibaba.fenxiao',
215
+            'alibaba.pifatuan.product.detail.list',
216
+            2,
217
+            $businessParams
218
+        );
219
+
220
+        if ($result === false) {
221
+            return false;
222
+        }
223
+
224
+        // 临时:直接返回原始数据,方便查看结构
225
+        return $result;
226
+    }
227
+
228
+    /**
229
+     * 格式化商品详情结果
230
+     *
231
+     * 1688商品详情API返回结构:
232
+     * {
233
+     *   "result": {
234
+     *     "success": true,
235
+     *     "code": "0",
236
+     *     "result": {
237
+     *       "productInfo": { 商品详细信息 },
238
+     *       "skuList": [ SKU列表 ],
239
+     *       "productImgList": [ 图片列表 ]
240
+     *     }
241
+     *   }
242
+     * }
243
+     *
244
+     * @param array $result
245
+     * @return array
246
+     */
247
+    protected function formatDetailResult(array $result): array
248
+    {
249
+        $data = $result['result'] ?? $result;
250
+        $detailData = $data['result'] ?? [];
251
+
252
+        if (empty($detailData)) {
253
+            return [];
254
+        }
255
+
256
+        $productInfo = $detailData['productInfo'] ?? $detailData;
257
+        $skuList = $detailData['skuList'] ?? [];
258
+        $productImgList = $detailData['productImgList'] ?? [];
259
+
260
+        // 格式化SKU
261
+        $formattedSkus = [];
262
+        if (!empty($skuList)) {
263
+            foreach ($skuList as $sku) {
264
+                $formattedSkus[] = [
265
+                    'skuId' => $sku['skuId'] ?? '',
266
+                    'specId' => $sku['specId'] ?? '',
267
+                    'price' => $sku['price'] ?? $sku['retailPrice'] ?? 0,
268
+                    'salePrice' => $sku['salePrice'] ?? $sku['price'] ?? 0,
269
+                    'stock' => $sku['stock'] ?? $sku['availableStock'] ?? 0,
270
+                    'specName' => $sku['specName'] ?? $sku['name'] ?? '',
271
+                    'image' => $sku['image'] ?? '',
272
+                ];
273
+            }
274
+        }
275
+
276
+        // 格式化图片列表
277
+        $imageList = [];
278
+        if (!empty($productImgList)) {
279
+            foreach ($productImgList as $img) {
280
+                $url = is_string($img) ? $img : ($img['url'] ?? $img['imgUrl'] ?? '');
281
+                if (!empty($url)) {
282
+                    $imageList[] = $url;
283
+                }
284
+            }
285
+        }
286
+
287
+        // 主图
288
+        $mainImage = $productInfo['imgUrl'] ?? $productInfo['mainImage'] ?? $productInfo['mainPicture'] ?? '';
289
+        if (empty($mainImage) && !empty($imageList)) {
290
+            $mainImage = $imageList[0];
291
+        }
292
+
293
+        // 服务标签
294
+        $serviceTags = [];
295
+        if (!empty($productInfo['serviceList'])) {
296
+            foreach ($productInfo['serviceList'] as $service) {
297
+                $serviceTags[] = $service['name'] ?? '';
298
+            }
299
+        }
300
+
301
+        return [
302
+            // 基础信息
303
+            'productId' => $productInfo['itemId'] ?? $productInfo['productId'] ?? $productInfo['offerId'] ?? 0,
304
+            'productType' => $productInfo['productType'] ?? '',
305
+            'categoryId' => $productInfo['categoryId'] ?? $productInfo['catId'] ?? 0,
306
+            'categoryName' => $productInfo['categoryName'] ?? '',
307
+
308
+            // 标题与描述
309
+            'title' => $productInfo['title'] ?? $productInfo['subject'] ?? $productInfo['name'] ?? '',
310
+            'description' => $productInfo['description'] ?? $productInfo['detail'] ?? '',
311
+
312
+            // 价格信息
313
+            'price' => $productInfo['minPrice'] ?? $productInfo['price'] ?? $productInfo['offerPrice'] ?? 0,
314
+            'salePrice' => $productInfo['maxPrice'] ?? $productInfo['salePrice'] ?? $productInfo['price'] ?? 0,
315
+            'minPrice' => $productInfo['minPrice'] ?? $productInfo['price'] ?? 0,
316
+            'maxPrice' => $productInfo['maxPrice'] ?? $productInfo['price'] ?? 0,
317
+
318
+            // 库存信息
319
+            'stock' => $productInfo['stock'] ?? $productInfo['totalAvailableStock'] ?? 0,
320
+            'unit' => $productInfo['unit'] ?? '件',
321
+
322
+            // 图片信息
323
+            'mainImage' => $mainImage,
324
+            'imageList' => $imageList,
325
+
326
+            // SKU列表
327
+            'skuList' => $formattedSkus,
328
+
329
+            // 销售信息
330
+            'monthSales' => $productInfo['monthSales'] ?? $productInfo['salesCnt90d'] ?? 0,
331
+            'totalSales' => $productInfo['totalSales'] ?? 0,
332
+
333
+            // 服务标签
334
+            'serviceTags' => $serviceTags,
335
+
336
+            // 分销属性
337
+            'isDistribution' => $productInfo['isDistribution'] ?? false,
338
+            'isStrictSelected' => $productInfo['isStrictSelected'] ?? false,
339
+            'distributorPrice' => $productInfo['distributorPrice'] ?? 0,
340
+
341
+            // 店铺信息
342
+            'shopName' => $productInfo['shopName'] ?? $productInfo['companyName'] ?? '',
343
+            'shopId' => $productInfo['shopId'] ?? $productInfo['memberId'] ?? '',
344
+
345
+            // 链接
346
+            'detailUrl' => $productInfo['detailUrl'] ?? $productInfo['offerUrl'] ?? '',
347
+        ];
348
+    }
349
+}

+ 18 - 0
config/third_party.php

@@ -8,5 +8,23 @@ return [
8 8
         'adzoneId' => 116174700455,
9 9
         // 商品筛选-后台类目ID。用,分割,最大10个
10 10
         'cat' => '50016422,50012486,50008164,50002766,50050359,50022703,50000436,50011130,50011277,50020579'
11
+    ],
12
+    // 1688分销严选采购解决方案(分销买家版)
13
+    'alibaba_agent' => [
14
+        'appKey' => '6512262',
15
+        'appSecret' => 'uNPRZE7895',
16
+        'accessToken' => 'dc8eab20-f10f-4688-9e45-9ebfa182c7c2',
17
+        // 授权回调地址
18
+        // 'redirectUri' => 'https://yourdomain.com/api/alibaba/callback',
19
+        // 1688 API网关地址
20
+        'gatewayUrl' => 'https://gw.open.1688.com/openapi/',
21
+        // 协议版本
22
+        'version' => '1',
23
+        // 分销买家ID(在1688分销平台获取)
24
+        'distributorId' => '',
25
+        // 日志开关
26
+        'logEnabled' => true,
27
+        // 日志目录
28
+        'logPath' => runtime_path() . 'alibaba/',
11 29
     ]
12 30
 ];

+ 7 - 0
route/admin.php

@@ -713,6 +713,13 @@ Route::group(config('admin.api_admin_prefix') . '/', function () {
713 713
         Route::get('/gong/category', 'admin.gong.Goods/category');
714 714
 
715 715
 
716
+        //1688分销严选商品
717
+        Route::group('alibaba/goods',function(){
718
+            Route::get('lst', '/lst')->name('alibabaGoodsLst');
719
+            Route::get('detail', '/detail')->name('alibabaGoodsDetail');
720
+        })->prefix('admin.alibaba.Goods');
721
+
722
+
716 723
         //直播间
717 724
         Route::group('broadcast/room', function () {
718 725
             Route::get('lst', '/lst')->name('systemBroadcastRoomLst');