# 店匠 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 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> request = new HttpEntity<>(requestBody, headers); try { ResponseEntity 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 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