记录tenant和token-获取商品信息.md
12.1 KB
让我详细解释这些任务,并查看相关的代码实现:
[3 tools called]
📋 详细任务解释
让我逐一解释这些开发任务:
1️⃣ OAuth 的迁移 - 参考 Go 的实现
任务目标: 将现有的 Go 语言 OAuth 实现迁移到 Java 项目中
具体工作:
// 需要实现的 Java 端点:
GET /oauth/install?shop={shop_domain} // 处理APP安装请求
GET /oauth/callback?code=xxx&shop=xxx // 处理OAuth授权回调
POST /partner/oauth/token // 换取/刷新 Access Token
参考的 Go 代码功能:
- 生成授权 URL 并重定向
- 处理授权回调
- 用 code 换取 Access Token
- 解析 Token 响应并存储
2️⃣ AccessToken 的存储 - 存储到 shoplazza_shop_config
任务目标: OAuth 成功后,将 Token 信息保存到数据库
示意图:
店匠平台 搜索SaaS平台
----------- ----------------
[应用市场]
↓
[商家安装APP] --------→ OAuth授权流程
↓ ↓
[商家授权成功] --------→ 【第2项】创建租户+存储Token
↓
system_tenant (新建)
shoplazza_shop_config (新建)
存储 AccessToken 和 RefreshToken
↓
【第3项】定时刷新Token
Token 的获取和使用流程
sequenceDiagram
participant 商家
participant 店匠
participant 你的后端
participant 数据库
Note over 商家,你的后端: 1. OAuth 授权阶段
商家->>店匠: 安装 APP
店匠->>你的后端: 跳转授权
商家->>店匠: 同意授权
店匠->>你的后端: 回调带 code
你的后端->>店匠: 用 code 换 Token
店匠->>你的后端: 返回 Access Token
你的后端->>数据库: 存储到 shoplazza_shop_config
Note over 你的后端,数据库: 2. 注册 Webhook 阶段
你的后端->>数据库: 读取 Access Token
你的后端->>店匠: 注册 Webhook (带 Access Token)
店匠->>你的后端: Webhook 注册成功
核心逻辑:
@Transactional
public void handleOAuthCallback(TokenResponse tokenResponse) {
// 1. 检查租户是否存在,不存在则创建
Tenant tenant = tenantMapper.selectByStoreId(storeId);
if (tenant == null) {
tenant = new Tenant();
tenant.setName(storeName);
tenantMapper.insert(tenant); // 👈 创建新租户
}
// 2. 创建或更新店铺配置
ShopConfig shop = shopConfigMapper.selectByStoreId(storeId);
if (shop == null) {
shop = new ShopConfig();
shop.setTenantId(tenant.getId());
shop.setStoreId(storeId);
shop.setStoreName(storeName);
}
// 3. 保存 Token 信息
shop.setAccessToken(tokenResponse.getAccessToken()); // 👈 存储
shop.setRefreshToken(tokenResponse.getRefreshToken()); // 👈 存储
shop.setTokenExpiresAt(tokenResponse.getExpiresAt()); // 👈 存储
shop.setLocale(tokenResponse.getLocale());
shop.setStatus("active");
shopConfigMapper.insertOrUpdate(shop);
}
数据表: shoplazza_shop_config(已设计在文档第4章)
📊 token数据库表关系
-- shoplazza_shop_config 表中存储的数据
+----------+----------------+----------------------------------------+
| store_id | store_name | access_token |
+----------+----------------+----------------------------------------+
| 2286274 | 47167113-1 | V2WDYgkTvrN68QCESZ9eHb3EjpR6EB... | 👈 OAuth时保存
+----------+----------------+----------------------------------------+
↓
注册 Webhook 时读取使用
🔐 Token 的两种用途
这个 Access Token 在你的系统中有两大用途:
拉取数据 - 调用店匠 Admin API
- 拉取商品:
GET /openapi/2022-01/products - 拉取订单:
GET /openapi/2022-01/orders - 拉取客户:
GET /openapi/2022-01/tenants
- 拉取商品:
注册 Webhook - 让店匠主动推送数据变更
- 注册:
POST /openapi/2022-01/webhooks(需要 Token) - 接收:店匠推送到你的
/webhook/shoplazza/{storeId}端点(不需要 Token)
- 注册:
⚠️ 注意事项
// 注册 Webhook 前,确保 Token 有效
public void registerWebhooks(Long shopConfigId) {
ShopConfig shop = shopConfigMapper.selectById(shopConfigId);
// 检查 Token 是否过期
if (shop.getTokenExpiresAt().before(new Date())) {
// Token 已过期,先刷新
tokenService.refreshToken(shop);
shop = shopConfigMapper.selectById(shopConfigId); // 重新读取
}
// 使用有效的 Token 注册 Webhook
String accessToken = shop.getAccessToken();
// ... 注册逻辑
}
3️⃣ RefreshToken 的实现 - 基于定时任务,需考虑对多家店铺的处理
任务目标: 自动刷新即将过期的 Access Token
实现方式:
@Scheduled(cron = "0 0 2 * * ?") // 每天凌晨2点执行
public void refreshExpiringTokens() {
// 1. 查询7天内过期的所有店铺
DateTime sevenDaysLater = DateTime.now().plusDays(7);
List<ShopConfig> shops = shopConfigMapper.selectExpiringTokens(sevenDaysLater);
// 2. 遍历每个店铺,刷新 Token
for (ShopConfig shop : shops) {
try {
TokenResponse newToken = oauthClient.refreshToken(
shop.getRefreshToken(),
clientId,
clientSecret
);
// 3. 更新数据库中的 Token
shop.setAccessToken(newToken.getAccessToken());
shop.setRefreshToken(newToken.getRefreshToken());
shop.setTokenExpiresAt(newToken.getExpiresAt());
shopConfigMapper.updateById(shop);
log.info("Token refreshed for shop: {}", shop.getStoreName());
} catch (Exception e) {
log.error("Failed to refresh token for shop: {}", shop.getStoreName(), e);
// 发送告警通知
}
}
}
关键点:
- ✅ 批量处理多家店铺
- ✅ 提前7天刷新(避免过期)
- ✅ 异常处理和告警
4️⃣ 批量拉取商品信息的优化 - 验证分页查询
任务目标: 完善商品数据同步,确保分页正确处理
当前问题: 代码可能只拉取了第一页数据,未正确遍历所有页
需要验证和优化:
public void syncProducts(Long shopConfigId) {
ShopConfig shop = shopConfigMapper.selectById(shopConfigId);
int page = 1;
int limit = 50;
boolean hasMore = true;
while (hasMore) { // 👈 关键:循环直到没有更多数据
// 调用店匠 API
String url = String.format(
"https://%s/openapi/2022-01/products?page=%d&limit=%d",
shop.getStoreDomain(), page, limit
);
ProductListResponse response = apiClient.get(url, shop.getAccessToken());
// 判断是否还有更多数据
if (response.getProducts() == null || response.getProducts().isEmpty()) {
hasMore = false; // 👈 没有数据了,退出循环
break;
}
// 保存当前页的商品
for (ProductDto product : response.getProducts()) {
saveProduct(shop.getTenantId(), shop.getStoreId(), product);
}
page++; // 👈 下一页
Thread.sleep(100); // 避免触发速率限制
}
}
验证要点:
- ✅ 分页参数正确传递
- ✅ 循环终止条件正确
- ✅ 处理空页面情况
- ✅ 速率限制控制
5️⃣ 批量拉取客户信息的优化 - 验证分页查询
任务目标: 与商品同步类似,完善客户数据同步
实现逻辑:
public void syncTenants(Long shopConfigId) {
// 与 syncProducts 类似,遍历所有分页
String url = "https://{shop}/openapi/2022-01/tenants?page={page}&limit=50";
// 循环拉取所有页
// 保存到 shoplazza_tenant 和 shoplazza_tenant_address 表
}
6️⃣ 批量拉取订单信息的优化 - 验证分页查询
任务目标: 完善订单数据同步
实现逻辑:
public void syncOrders(Long shopConfigId) {
String url = "https://{shop}/openapi/2022-01/orders?page={page}&limit=50";
// 保存到 shoplazza_order 和 shoplazza_order_item 表
}
7️⃣ 批量拉取店铺信息的实现 - 新增实现,需设计对应的数据库表
任务目标: 拉取店铺的详细配置信息
API 调用:
GET /openapi/2022-01/shop
可能的响应字段:
{
"id": "2286274",
"name": "47167113-1",
"domain": "47167113-1.myshoplaza.com",
"email": "shop@example.com",
"currency": "USD",
"timezone": "Asia/Shanghai",
"locale": "zh-CN",
"address": {...},
"phone": "+86 123456789"
}
需要设计的数据表:
CREATE TABLE `shoplazza_shop_info` (
`id` BIGINT NOT NULL AUTO_INCREMENT,
`store_id` VARCHAR(64) NOT NULL,
`shop_name` VARCHAR(255),
`domain` VARCHAR(255),
`email` VARCHAR(255),
`currency` VARCHAR(16),
`timezone` VARCHAR(64),
`locale` VARCHAR(16),
`phone` VARCHAR(64),
`address` JSON, -- 存储完整地址信息
`plan_name` VARCHAR(64), -- 套餐名称
`created_at` DATETIME,
`updated_at` DATETIME,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_store_id` (`store_id`)
) COMMENT='店铺详细信息表';
8️⃣ 注册店铺的 Webhook - 新增实现,需考虑安全验证
任务目标: 为每个店铺注册 Webhook,接收实时数据变更通知
实现步骤:
A. 注册 Webhook(后端主动调用)
@Service
public class WebhookService {
private static final List<String> WEBHOOK_TOPICS = Arrays.asList(
"products/create", "products/update", "products/delete",
"orders/create", "orders/updated", "tenants/create"
);
public void registerWebhooks(Long shopConfigId) {
ShopConfig shop = shopConfigMapper.selectById(shopConfigId);
String webhookUrl = "https://your-domain.com/webhook/shoplazza/" + shop.getStoreId();
for (String topic : WEBHOOK_TOPICS) {
// 调用店匠 API 注册
apiClient.post(
"https://" + shop.getStoreDomain() + "/openapi/2022-01/webhooks",
shop.getAccessToken(),
Map.of("address", webhookUrl, "topic", topic)
);
}
}
}
B. 接收 Webhook(店匠主动推送)
@RestController
@RequestMapping("/webhook/shoplazza")
public class WebhookController {
@PostMapping("/{storeId}")
public ResponseEntity<String> handleWebhook(
@PathVariable String storeId,
@RequestHeader("X-Shoplazza-Hmac-Sha256") String signature, // 👈 安全验证
@RequestHeader("X-Shoplazza-Topic") String topic,
@RequestBody String payload) {
// 1. 验证签名(安全验证)
if (!verifySignature(payload, signature, clientSecret)) {
return ResponseEntity.status(401).body("Invalid signature");
}
// 2. 异步处理事件
webhookService.processAsync(storeId, topic, payload);
// 3. 立即返回 200(店匠要求3秒内响应)
return ResponseEntity.ok("OK");
}
// HMAC-SHA256 签名验证
private boolean verifySignature(String payload, String signature, String secret) {
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(secret.getBytes(), "HmacSHA256"));
byte[] hash = mac.doFinal(payload.getBytes());
String computed = Base64.getEncoder().encodeToString(hash);
return computed.equals(signature);
}
}
安全验证关键点:
- ✅ 使用 HMAC-SHA256 验证签名
- ✅ 签名密钥使用 APP 的 Client Secret
- ✅ 3秒内返回响应
- ✅ 异步处理事件,避免超时