06e79082
tangwang
接口优化
|
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
|
让我详细解释这些任务,并查看相关的代码实现:
[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`
|
ae5a294d
tangwang
命名修改、代码清理
|
130
|
- 拉取客户:`GET /openapi/2022-01/tenants`
|
06e79082
tangwang
接口优化
|
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
|
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
|
ae5a294d
tangwang
命名修改、代码清理
|
258
|
public void syncTenants(Long shopConfigId) {
|
06e79082
tangwang
接口优化
|
259
|
// 与 syncProducts 类似,遍历所有分页
|
ae5a294d
tangwang
命名修改、代码清理
|
260
|
String url = "https://{shop}/openapi/2022-01/tenants?page={page}&limit=50";
|
06e79082
tangwang
接口优化
|
261
262
|
// 循环拉取所有页
|
ae5a294d
tangwang
命名修改、代码清理
|
263
|
// 保存到 shoplazza_tenant 和 shoplazza_tenant_address 表
|
06e79082
tangwang
接口优化
|
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
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
|
}
```
---
## 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",
|
ae5a294d
tangwang
命名修改、代码清理
|
345
|
"orders/create", "orders/updated", "tenants/create"
|
06e79082
tangwang
接口优化
|
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
|
);
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秒内返回响应
- ✅ 异步处理事件,避免超时
---
|