Blame view

docs/店匠相关资料/SHOPLAZZA_TOKEN_REFRESH.md 8.93 KB
3bb1af6b   tangwang   tenant1和tenant2 m...
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
  # 店匠 Token 刷新文档
  
  ## ✅ 正确的刷新 Token 端点
  
  根据店匠官方文档,刷新 Token 的端点**不是** `partners.shoplazza.com`,而是**店铺域名**下的端点:
  
  ```
  POST https://{store_name}.myshoplaza.com/admin/oauth/token
  ```
  
  ## 📋 请求格式
  
  ### curl 命令
  
  ```bash
  curl --location --request POST 'https://47167113-1.myshoplaza.com/admin/oauth/token' \
  --header 'Content-Type: application/json' \
  --header 'Accept: application/json' \
  --data-raw '{
      "client_id": "m8F9PrPnxpyrlz4ONBWRoINsa5xyNT4Qd-Fh_h7o1es",
      "client_secret": "m2cDNrBqAa8TKeridXd4eXnhi9E7pda2gKXet_72rjo",
      "refresh_token": "NjYhBexdDZ0NE87Bg8Xerx1rFRnHvMQ9XDy_PITV1ME",
      "grant_type": "refresh_token",
      "redirect_uri": "https://saas-ai-api.essa.top/search/oauth_sdk/redirect_uri"
  }'
  ```
  
  ### 请求参数
  
  | 参数 | 类型 | 必填 | 说明 |
  |------|------|------|------|
  | `client_id` | String | ✅ | APP 的 Client ID |
  | `client_secret` | String | ✅ | APP 的 Client Secret |
  | `refresh_token` | String | ✅ | 之前获得的 Refresh Token |
  | `grant_type` | String | ✅ | 固定值:`"refresh_token"` |
  | `redirect_uri` | String | ✅ | OAuth 配置的 Redirect URI |
  
  ## 📥 响应格式
  
  ### 成功响应
  
  ```json
  {
    "access_token": "R77yIJLfYy5mGWs_MGLsw7dT67iAIeHxwJga5psB5Yg",
    "token_type": "Bearer",
    "expires_in": 31556951,
    "refresh_token": "R3bAhXUSAFZ7V5Kzk7YEJXdepNAQbefi85QyG4XiEkY",
    "scope": "read_shop write_shop read_product read_order read_customer read_app_proxy",
    "created_at": 1763037342,
    "store_id": "2286274",
    "store_name": "47167113-1",
    "expires_at": 1794594294,
    "locale": "zh-CN"
  }
  ```
  
  ### 响应字段说明
  
  | 字段 | 说明 |
  |------|------|
  | `access_token` | 新的 Access Token(需要更新数据库) |
  | `refresh_token` | 新的 Refresh Token(需要更新数据库) |
  | `expires_at` | 新的过期时间戳 |
  | `store_id` | 店铺 ID |
  | `store_name` | 店铺名称 |
  
  **⚠️ 重要:** 刷新后会返回**新的** `access_token` 和 `refresh_token`,必须更新数据库!
  
  ## 💻 代码实现示例
  
  ### Java 实现
  
  ```java
  @Service
  public class ShoplazzaTokenService {
      
      @Value("${shoplazza.oauth.client-id}")
      private String clientId;
      
      @Value("${shoplazza.oauth.client-secret}")
      private String clientSecret;
      
      @Value("${shoplazza.oauth.redirect-uri}")
      private String redirectUri;
      
      @Autowired
      private RestTemplate restTemplate;
      
      @Autowired
      private ShopConfigMapper shopConfigMapper;
      
      /**
       * 刷新 Access Token
       */
      public TokenResponse refreshToken(String storeDomain, String refreshToken) {
          // 注意:端点是店铺域名,不是 partners.shoplazza.com
          String url = String.format("https://%s/admin/oauth/token", storeDomain);
          
          Map<String, String> requestBody = new HashMap<>();
          requestBody.put("client_id", clientId);
          requestBody.put("client_secret", clientSecret);
          requestBody.put("refresh_token", refreshToken);
          requestBody.put("grant_type", "refresh_token");
          requestBody.put("redirect_uri", redirectUri);  // ← 重要:必须包含
          
          HttpHeaders headers = new HttpHeaders();
          headers.setContentType(MediaType.APPLICATION_JSON);
          
          HttpEntity<Map<String, String>> request = new HttpEntity<>(requestBody, headers);
          
          try {
              ResponseEntity<TokenResponse> response = restTemplate.postForEntity(
                  url,
                  request,
                  TokenResponse.class
              );
              
              if (response.getStatusCode().is2xxSuccessful() && response.getBody() != null) {
                  return response.getBody();
              }
              
              throw new BusinessException("Failed to refresh token: " + response.getStatusCode());
              
          } catch (HttpClientErrorException e) {
              log.error("Token refresh failed: {}", e.getResponseBodyAsString());
              throw new BusinessException("Token refresh failed: " + e.getMessage());
          }
      }
      
      /**
       * 刷新指定店铺的 Token
       */
      @Transactional
      public void refreshShopToken(Long shopConfigId) {
          ShopConfig shop = shopConfigMapper.selectById(shopConfigId);
          if (shop == null) {
              throw new BusinessException("Shop not found");
          }
          
          if (StringUtils.isEmpty(shop.getRefreshToken())) {
              throw new BusinessException("Refresh token not found for shop: " + shop.getStoreName());
          }
          
          try {
              // 调用刷新接口(使用店铺域名)
              TokenResponse newToken = refreshToken(
                  shop.getStoreDomain(),  // 如:47167113-1.myshoplaza.com
                  shop.getRefreshToken()
              );
              
              // 更新数据库
              shop.setAccessToken(newToken.getAccessToken());
              shop.setRefreshToken(newToken.getRefreshToken());
              shop.setTokenExpiresAt(newToken.getExpiresAt());
              shop.setUpdatedAt(new Date());
              
              shopConfigMapper.updateById(shop);
              
              log.info("Token refreshed successfully for shop: {}", shop.getStoreName());
              
          } catch (Exception e) {
              log.error("Failed to refresh token for shop: {}", shop.getStoreName(), e);
              throw new BusinessException("Token refresh failed", e);
          }
      }
  }
  ```
  
  ### Python 实现
  
  ```python
  import requests
  from datetime import datetime
  
  class ShoplazzaTokenService:
      def __init__(self, client_id, client_secret, redirect_uri):
          self.client_id = client_id
          self.client_secret = client_secret
          self.redirect_uri = redirect_uri
      
      def refresh_token(self, store_domain, refresh_token):
          """刷新 Access Token"""
          # 注意:端点是店铺域名
          url = f"https://{store_domain}/admin/oauth/token"
          
          payload = {
              "client_id": self.client_id,
              "client_secret": self.client_secret,
              "refresh_token": refresh_token,
              "grant_type": "refresh_token",
              "redirect_uri": self.redirect_uri  # ← 重要:必须包含
          }
          
          headers = {
              "Content-Type": "application/json",
              "Accept": "application/json"
          }
          
          try:
              response = requests.post(
                  url,
                  json=payload,
                  headers=headers,
                  timeout=10
              )
              
              response.raise_for_status()
              return response.json()
              
          except requests.exceptions.RequestException as e:
              print(f"Token refresh failed: {e}")
              raise
  ```
  
  ## ⏰ 定时刷新策略
  
  ### 提前刷新(推荐)
  
  ```java
  @Scheduled(cron = "0 0 2 * * ?")  // 每天凌晨2点执行
  public void refreshExpiringTokens() {
      // 查询7天内过期的 Token
      DateTime sevenDaysLater = DateTime.now().plusDays(7);
      List<ShopConfig> shops = shopConfigMapper.selectExpiringTokens(sevenDaysLater);
      
      for (ShopConfig shop : shops) {
          try {
              refreshShopToken(shop.getId());
              log.info("Token refreshed for shop: {}", shop.getStoreName());
          } catch (Exception e) {
              log.error("Failed to refresh token for shop: {}", shop.getStoreName(), e);
              // 发送告警通知
          }
      }
  }
  ```
  
  ### API 调用时检查并刷新
  
  ```java
  public String getValidAccessToken(String storeId) {
      ShopConfig shop = shopConfigMapper.selectByStoreId(storeId);
      if (shop == null) {
          throw new BusinessException("Shop not found: " + storeId);
      }
      
      // 检查是否即将过期(提前1小时)
      DateTime oneHourLater = DateTime.now().plusHours(1);
      if (shop.getTokenExpiresAt().isBefore(oneHourLater)) {
          // 刷新 Token
          refreshShopToken(shop.getId());
          // 重新查询
          shop = shopConfigMapper.selectByStoreId(storeId);
      }
      
      return shop.getAccessToken();
  }
  ```
  
  ## ⚠️ 关键要点
  
  ### 1. 端点差异
  
  | 用途 | 端点 |
  |------|------|
  | **获取 Token(初始授权)** | `https://partners.shoplazza.com/partner/oauth/token` |
  | **刷新 Token** | `https://{store_domain}.myshoplaza.com/admin/oauth/token` |
  
  ### 2. 必需参数
  
  -`client_id`
  -`client_secret`
  -`refresh_token`
  -`grant_type: "refresh_token"`
  -**`redirect_uri`** ← 这个很重要,之前测试失败就是因为缺少这个!
  
  ### 3. 更新数据库
  
  刷新成功后,**必须**更新数据库中的:
  - `access_token`
  - `refresh_token`
  - `token_expires_at`
  
  ## 📊 测试结果
  
  **测试成功!**
  
  - 旧 Access Token: `6GAjQbN51YS-N5l0e2Oxlc19iVn4X8FCeuHQ7df4DSA`
  - 新 Access Token: `R77yIJLfYy5mGWs_MGLsw7dT67iAIeHxwJga5psB5Yg`
  - 新 Refresh Token: `R3bAhXUSAFZ7V5Kzk7YEJXdepNAQbefi85QyG4XiEkY`
  - 新过期时间: `1794594294` (2026-11-14)
  
  ## 🔗 参考文档
  
  - 店匠官方文档:刷新 Token 章节
  - OAuth 2.0 标准:RFC 6749