SHOPLAZZA_TOKEN_REFRESH.md 8.93 KB

店匠 Token 刷新文档

✅ 正确的刷新 Token 端点

根据店匠官方文档,刷新 Token 的端点不是 partners.shoplazza.com,而是店铺域名下的端点:

POST https://{store_name}.myshoplaza.com/admin/oauth/token

📋 请求格式

curl 命令

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

📥 响应格式

成功响应

{
  "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_tokenrefresh_token,必须更新数据库!

💻 代码实现示例

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 实现

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

⏰ 定时刷新策略

提前刷新(推荐)

@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 调用时检查并刷新

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