让我详细解释这些任务，并查看相关的代码实现：

[3 tools called]

## 📋 详细任务解释

让我逐一解释这些开发任务：

---

## 1️⃣ **OAuth 的迁移 - 参考 Go 的实现**

**任务目标：** 将现有的 Go 语言 OAuth 实现迁移到 Java 项目中

**具体工作：**
```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 的获取和使用流程

```mermaid
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 注册成功
```

**核心逻辑：**
```java
@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数据库表关系

```sql
-- shoplazza_shop_config 表中存储的数据
+----------+----------------+----------------------------------------+
| store_id | store_name     | access_token                           |
+----------+----------------+----------------------------------------+
| 2286274  | 47167113-1     | V2WDYgkTvrN68QCESZ9eHb3EjpR6EB...     |  👈 OAuth时保存
+----------+----------------+----------------------------------------+
                                     ↓
                              注册 Webhook 时读取使用
```

### 🔐 Token 的两种用途

**这个 Access Token 在你的系统中有两大用途：**

1. **拉取数据** - 调用店匠 Admin API
   - 拉取商品：`GET /openapi/2022-01/products`
   - 拉取订单：`GET /openapi/2022-01/orders`
   - 拉取客户：`GET /openapi/2022-01/tenants`

2. **注册 Webhook** - 让店匠主动推送数据变更
   - 注册：`POST /openapi/2022-01/webhooks`（需要 Token）
   - 接收：店匠推送到你的 `/webhook/shoplazza/{storeId}` 端点（不需要 Token）

### ⚠️ 注意事项

```java
// 注册 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

**实现方式：**

```java
@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️⃣ **批量拉取商品信息的优化 - 验证分页查询**

**任务目标：** 完善商品数据同步，确保分页正确处理

**当前问题：** 代码可能只拉取了第一页数据，未正确遍历所有页

**需要验证和优化：**

```java
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️⃣ **批量拉取客户信息的优化 - 验证分页查询**

**任务目标：** 与商品同步类似，完善客户数据同步

**实现逻辑：**
```java
public void syncTenants(Long shopConfigId) {
    // 与 syncProducts 类似，遍历所有分页
    String url = "https://{shop}/openapi/2022-01/tenants?page={page}&limit=50";
    
    // 循环拉取所有页
    // 保存到 shoplazza_tenant 和 shoplazza_tenant_address 表
}
```

---

## 6️⃣ **批量拉取订单信息的优化 - 验证分页查询**

**任务目标：** 完善订单数据同步

**实现逻辑：**
```java
public void syncOrders(Long shopConfigId) {
    String url = "https://{shop}/openapi/2022-01/orders?page={page}&limit=50";
    
    // 保存到 shoplazza_order 和 shoplazza_order_item 表
}
```

---

## 7️⃣ **批量拉取店铺信息的实现 - 新增实现，需设计对应的数据库表**

**任务目标：** 拉取店铺的详细配置信息

**API 调用：**
```bash
GET /openapi/2022-01/shop
```

**可能的响应字段：**
```json
{
  "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"
}
```

**需要设计的数据表：**
```sql
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（后端主动调用）

```java
@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（店匠主动推送）

```java
@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秒内返回响应
- ✅ 异步处理事件，避免超时

---



















