Compare commits
1 Commits
0265a642d8
...
3f5fb6adba
| Author | SHA1 | Date |
|---|---|---|
|
|
3f5fb6adba |
|
|
@ -0,0 +1,184 @@
|
|||
# Permutation页面列表显示问题分析和修复方案
|
||||
|
||||
## 问题分析
|
||||
|
||||
经过代码分析,发现了以下几个可能导致列表不显示的问题:
|
||||
|
||||
### 1. API路径不匹配
|
||||
前端代码中引用的API函数名与后端控制器中的路径不一致:
|
||||
- 前端:`productcontrollerGetcategoriesall`、`productcontrollerGetcategoryattributes`、`productcontrollerGetproductlist`
|
||||
- 后端实际的API路径:`/product/categories/all`、`/product/category/:id/attributes`、`/product/list`
|
||||
|
||||
### 2. 数据格式问题
|
||||
- `getCategoryAttributes`返回的数据结构与前端期望的不匹配
|
||||
- 属性值获取逻辑可能存在问题
|
||||
|
||||
### 3. 组合生成逻辑问题
|
||||
- 在生成排列组合时,数据结构和键值对应可能不正确
|
||||
|
||||
## 修复方案
|
||||
|
||||
### 后端修复
|
||||
|
||||
1. **修改getCategoryAttributes方法** - 在`/Users/zksu/Developer/work/workcode/API/src/service/product.service.ts`中:
|
||||
|
||||
```typescript
|
||||
// 获取分类下的属性配置
|
||||
async getCategoryAttributes(categoryId: number): Promise<any[]> {
|
||||
const category = await this.categoryModel.findOne({
|
||||
where: { id: categoryId },
|
||||
relations: ['attributes', 'attributes.attributeDict', 'attributes.attributeDict.items'],
|
||||
});
|
||||
|
||||
if (!category) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// 格式化返回,匹配前端期望的数据结构
|
||||
return category.attributes.map(attr => ({
|
||||
id: attr.id,
|
||||
dictId: attr.attributeDict.id,
|
||||
name: attr.attributeDict.name, // 用于generateKeyFromPermutation
|
||||
title: attr.attributeDict.title, // 用于列标题
|
||||
dict: {
|
||||
id: attr.attributeDict.id,
|
||||
name: attr.attributeDict.name,
|
||||
title: attr.attributeDict.title,
|
||||
items: attr.attributeDict.items || []
|
||||
}
|
||||
}));
|
||||
}
|
||||
```
|
||||
|
||||
2. **确保dict/items接口可用** - 检查字典项获取接口:
|
||||
|
||||
在`/Users/zksu/Developer/work/workcode/API/src/controller/dict.controller.ts`中添加或确认:
|
||||
|
||||
```typescript
|
||||
@Get('/items')
|
||||
async getDictItems(@Query('dictId') dictId: number) {
|
||||
try {
|
||||
const dict = await this.dictModel.findOne({
|
||||
where: { id: dictId },
|
||||
relations: ['items']
|
||||
});
|
||||
|
||||
if (!dict) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return dict.items || [];
|
||||
} catch (error) {
|
||||
return errorResponse(error?.message || error);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 前端修复建议
|
||||
|
||||
1. **添加错误处理和调试信息**:
|
||||
|
||||
```typescript
|
||||
// 在获取属性值的地方添加错误处理
|
||||
const fetchData = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
// 1. Fetch Attributes
|
||||
const attrRes = await productcontrollerGetcategoryattributes({
|
||||
id: categoryId,
|
||||
});
|
||||
console.log('Attributes response:', attrRes); // 调试用
|
||||
const attrs = Array.isArray(attrRes) ? attrRes : attrRes?.data || [];
|
||||
setAttributes(attrs);
|
||||
|
||||
// 2. Fetch Attribute Values (Dict Items)
|
||||
const valuesMap: Record<string, any[]> = {};
|
||||
for (const attr of attrs) {
|
||||
const dictId = attr.dict?.id || attr.dictId;
|
||||
if (dictId) {
|
||||
try {
|
||||
const itemsRes = await request('/dict/items', {
|
||||
params: { dictId },
|
||||
});
|
||||
console.log(`Dict items for ${attr.name}:`, itemsRes); // 调试用
|
||||
valuesMap[attr.name] = itemsRes || [];
|
||||
} catch (error) {
|
||||
console.error(`Failed to fetch items for dict ${dictId}:`, error);
|
||||
valuesMap[attr.name] = [];
|
||||
}
|
||||
}
|
||||
}
|
||||
setAttributeValues(valuesMap);
|
||||
|
||||
// 3. Fetch Existing Products
|
||||
await fetchProducts(categoryId);
|
||||
} catch (error) {
|
||||
console.error('Error in fetchData:', error);
|
||||
message.error('获取数据失败');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
2. **修复组合生成逻辑**:
|
||||
|
||||
```typescript
|
||||
// 修改generateKeyFromPermutation函数
|
||||
const generateKeyFromPermutation = (perm: any) => {
|
||||
const parts = Object.keys(perm).map((attrName) => {
|
||||
const valItem = perm[attrName];
|
||||
const val = valItem.name || valItem.value; // 兼容不同的数据格式
|
||||
return `${attrName}:${val}`;
|
||||
});
|
||||
return parts.sort().join('|');
|
||||
};
|
||||
|
||||
// 修改generateAttributeKey函数
|
||||
const generateAttributeKey = (attrs: any[]) => {
|
||||
const parts = attrs.map((a) => {
|
||||
const key = a.dict?.name || a.dictName || a.name;
|
||||
const val = a.name || a.value;
|
||||
return `${key}:${val}`;
|
||||
});
|
||||
return parts.sort().join('|');
|
||||
};
|
||||
```
|
||||
|
||||
3. **添加空状态处理**:
|
||||
|
||||
```typescript
|
||||
// 在ProTable中添加空状态提示
|
||||
<ProTable
|
||||
// ... 其他属性
|
||||
locale={{
|
||||
emptyText: permutations.length === 0 && !loading ? '暂无数据,请检查分类属性配置' : '暂无数据'
|
||||
}}
|
||||
/>
|
||||
```
|
||||
|
||||
## 调试步骤
|
||||
|
||||
1. **检查网络请求**:
|
||||
- 打开浏览器开发者工具
|
||||
- 检查 `/product/categories/all` 请求是否成功
|
||||
- 检查 `/product/category/:id/attributes` 请求返回的数据格式
|
||||
- 检查 `/dict/items?dictId=:id` 请求是否成功
|
||||
- 检查 `/product/list` 请求是否成功
|
||||
|
||||
2. **检查控制台日志**:
|
||||
- 查看属性数据是否正确加载
|
||||
- 查看属性值是否正确获取
|
||||
- 查看排列组合是否正确生成
|
||||
|
||||
3. **检查数据结构**:
|
||||
- 确认 `attributes` 数组是否正确
|
||||
- 确认 `attributeValues` 对象是否正确填充
|
||||
- 确认 `permutations` 数组是否正确生成
|
||||
|
||||
## 测试验证
|
||||
|
||||
1. 选择一个有属性配置的分类
|
||||
2. 确认属性有对应的字典项
|
||||
3. 检查排列组合是否正确显示
|
||||
4. 验证现有产品匹配是否正确
|
||||
|
|
@ -403,12 +403,9 @@ export class ShopyyAdapter implements ISiteAdapter {
|
|||
mapCreateOrderParams(data: Partial<UnifiedOrderDTO>): any {
|
||||
return data
|
||||
}
|
||||
mapProductQuery(query: UnifiedSearchParamsDTO): ShopyyProductQuery {
|
||||
return this.mapSearchParams(query)
|
||||
}
|
||||
|
||||
mapUpdateOrderParams(data: Partial<UnifiedOrderDTO>): any {
|
||||
// 构建 ShopYY 订单更新参数(仅包含传入的字段)
|
||||
// 构建 ShopYY 订单更新参数(仅包含传入的字段)
|
||||
const params: any = {};
|
||||
|
||||
// 仅当字段存在时才添加到更新参数中
|
||||
|
|
@ -668,7 +665,7 @@ export class ShopyyAdapter implements ISiteAdapter {
|
|||
const statusMap = {
|
||||
'pending': '100', // 100 未完成
|
||||
'processing': '110', // 110 待处理
|
||||
'completed': "180", // 180 已完成(确认收货)
|
||||
'completed': "180", // 180 已完成(确认收货)
|
||||
'cancelled': '190', // 190 取消
|
||||
};
|
||||
|
||||
|
|
@ -800,7 +797,7 @@ export class ShopyyAdapter implements ISiteAdapter {
|
|||
return status === 'publish' ? 1 : 0;
|
||||
};
|
||||
|
||||
// 构建 ShopYY 产品更新参数(仅包含传入的字段)
|
||||
// 构建 ShopYY 产品更新参数(仅包含传入的字段)
|
||||
const params: any = {};
|
||||
|
||||
// 仅当字段存在时才添加到更新参数中
|
||||
|
|
@ -819,7 +816,7 @@ export class ShopyyAdapter implements ISiteAdapter {
|
|||
params.inventory_quantity = data.stock_status === 'instock' ? (data.stock_quantity || 1) : 0;
|
||||
}
|
||||
|
||||
// 添加变体信息(如果存在)
|
||||
// 添加变体信息(如果存在)
|
||||
if (data.variations && data.variations.length > 0) {
|
||||
params.variants = data.variations.map((variation: UnifiedProductVariationDTO) => {
|
||||
const variationParams: any = {};
|
||||
|
|
@ -839,7 +836,7 @@ export class ShopyyAdapter implements ISiteAdapter {
|
|||
});
|
||||
}
|
||||
|
||||
// 添加图片信息(如果存在)
|
||||
// 添加图片信息(如果存在)
|
||||
if (data.images && data.images.length > 0) {
|
||||
params.images = data.images.map((image: any) => ({
|
||||
id: image.id,
|
||||
|
|
@ -849,12 +846,12 @@ export class ShopyyAdapter implements ISiteAdapter {
|
|||
}));
|
||||
}
|
||||
|
||||
// 添加标签信息(如果存在)
|
||||
// 添加标签信息(如果存在)
|
||||
if (data.tags && data.tags.length > 0) {
|
||||
params.tags = data.tags.map((tag: any) => tag.name || '');
|
||||
}
|
||||
|
||||
// 添加分类信息(如果存在)
|
||||
// 添加分类信息(如果存在)
|
||||
if (data.categories && data.categories.length > 0) {
|
||||
params.collections = data.categories.map((category: any) => ({
|
||||
id: category.id,
|
||||
|
|
@ -980,18 +977,6 @@ export class ShopyyAdapter implements ISiteAdapter {
|
|||
return product
|
||||
}
|
||||
|
||||
// 通过sku获取产品详情的私有方法
|
||||
private async getProductBySku(sku: string): Promise<UnifiedProductDTO> {
|
||||
// 使用Shopyy API的搜索功能通过sku查询产品
|
||||
const response = await this.getAllProducts({ where: {sku} });
|
||||
console.log('getProductBySku', response)
|
||||
const product = response?.[0]
|
||||
if (!product) {
|
||||
throw new Error(`未找到sku为${sku}的产品`);
|
||||
}
|
||||
return product
|
||||
}
|
||||
|
||||
async batchProcessProducts(
|
||||
data: { create?: any[]; update?: any[]; delete?: Array<string | number> }
|
||||
): Promise<any> {
|
||||
|
|
@ -1136,7 +1121,7 @@ export class ShopyyAdapter implements ISiteAdapter {
|
|||
}
|
||||
|
||||
mapUpdateVariationParams(data: Partial<UnifiedProductVariationDTO>): any {
|
||||
// 构建 ShopYY 变体更新参数(仅包含传入的字段)
|
||||
// 构建 ShopYY 变体更新参数(仅包含传入的字段)
|
||||
const params: any = {};
|
||||
|
||||
// 仅当字段存在时才添加到更新参数中
|
||||
|
|
@ -1194,10 +1179,7 @@ export class ShopyyAdapter implements ISiteAdapter {
|
|||
}
|
||||
|
||||
// ========== Webhook映射方法 ==========
|
||||
<<<<<<< HEAD
|
||||
=======
|
||||
|
||||
>>>>>>> 89d7d78 (refactor(api): 统一接口参数为对象形式并支持多条件查询)
|
||||
|
||||
mapUnifiedToPlatformWebhook(data: Partial<UnifiedWebhookDTO>) {
|
||||
return data
|
||||
|
|
@ -1310,12 +1292,12 @@ export class ShopyyAdapter implements ISiteAdapter {
|
|||
return stockStatus === 'instock' ? 1 : 0;
|
||||
};
|
||||
|
||||
shopyyOrderStatusMap = {//订单状态 100 未完成;110 待处理;180 已完成(确认收货); 190 取消;
|
||||
shopyyOrderStatusMap = {//订单状态 100 未完成;110 待处理;180 已完成(确认收货); 190 取消;
|
||||
[100]: OrderStatus.PENDING, // 100 未完成 转为 pending
|
||||
[110]: OrderStatus.PROCESSING, // 110 待处理 转为 processing
|
||||
// 已发货
|
||||
|
||||
[180]: OrderStatus.COMPLETED, // 180 已完成(确认收货) 转为 completed
|
||||
[180]: OrderStatus.COMPLETED, // 180 已完成(确认收货) 转为 completed
|
||||
[190]: OrderStatus.CANCEL // 190 取消 转为 cancelled
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -55,15 +55,6 @@ export class WooCommerceAdapter implements ISiteAdapter {
|
|||
mapUpdateVariationParams(data: UpdateVariationDTO) {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
batchProcessProducts?(data: BatchOperationDTO): Promise<BatchOperationResultDTO> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
mapCreateVariationParams(data: CreateVariationDTO) {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
mapUpdateVariationParams(data: UpdateVariationDTO) {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
// ========== 客户映射方法 ==========
|
||||
|
||||
|
|
@ -403,30 +394,53 @@ export class WooCommerceAdapter implements ISiteAdapter {
|
|||
// 将 WooCommerce 订单数据映射为统一订单DTO
|
||||
// 包含账单地址与收货地址以及创建与更新时间
|
||||
|
||||
// 产品操作方法
|
||||
async getProduct(id: string | number): Promise<UnifiedProductDTO> {
|
||||
// 获取单个产品详情并映射为统一产品DTO
|
||||
const api = (this.wpService as any).createApi(this.site, 'wc/v3');
|
||||
const res = await api.get(`products/${id}`);
|
||||
const product = res.data;
|
||||
// 映射物流追踪信息,将后端格式转换为前端期望的格式
|
||||
const fulfillments = (item.fulfillments || []).map((track: any) => ({
|
||||
tracking_number: track.tracking_number || '',
|
||||
shipping_provider: track.shipping_provider || '',
|
||||
shipping_method: track.shipping_method || '',
|
||||
status: track.status || '',
|
||||
date_created: track.date_created || '',
|
||||
items: track.items || [],
|
||||
}));
|
||||
|
||||
// 如果产品类型是 variable 且有变体 ID 列表,则加载完整的变体数据
|
||||
if (product.type === 'variable' && product.variations && Array.isArray(product.variations) && product.variations.length > 0) {
|
||||
try {
|
||||
// 批量获取该产品的所有变体数据
|
||||
const variations = await this.wpService.sdkGetAll(
|
||||
api,
|
||||
`products/${product.id}/variations`
|
||||
);
|
||||
// 将完整的变体数据添加到产品对象中
|
||||
product.variations = variations;
|
||||
} catch (error) {
|
||||
// 如果获取变体失败,保持原有的 ID 数组
|
||||
console.error(`获取产品 ${product.id} 的变体数据失败:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
return this.mapProduct(product);
|
||||
return {
|
||||
id: item.id,
|
||||
number: item.number,
|
||||
status: item.status,
|
||||
currency: item.currency,
|
||||
total: item.total,
|
||||
customer_id: item.customer_id,
|
||||
customer_email: item.billing?.email || '', // TODO 与 email 重复 保留一个即可
|
||||
email: item.billing?.email || '',
|
||||
customer_name: `${item.billing?.first_name || ''} ${item.billing?.last_name || ''}`.trim(),
|
||||
refunds: item.refunds?.map?.(refund => ({
|
||||
id: refund.id,
|
||||
reason: refund.reason,
|
||||
total: refund.total,
|
||||
})),
|
||||
line_items: (item.line_items as any[]).map(li => ({
|
||||
...li,
|
||||
productId: li.product_id,
|
||||
})),
|
||||
customer_ip_address: item.customer_ip_address ?? '',
|
||||
date_paid: item.date_paid ?? '',
|
||||
utm_source: item?.meta_data?.find(el => el.key === '_wc_order_attribution_utm_source')?.value || '',
|
||||
device_type: item?.meta_data?.find(el => el.key === '_wc_order_attribution_device_type')?.value || '',
|
||||
source_type: item?.meta_data?.find(el => el.key === '_wc_order_attribution_source_type')?.value || '',
|
||||
billing: item.billing,
|
||||
shipping: item.shipping,
|
||||
billing_full_address: this.buildFullAddress(item.billing),
|
||||
shipping_full_address: this.buildFullAddress(item.shipping),
|
||||
payment_method: item.payment_method_title,
|
||||
date_created: item.date_created,
|
||||
date_modified: item.date_modified,
|
||||
shipping_lines: item.shipping_lines,
|
||||
fee_lines: item.fee_lines,
|
||||
coupon_lines: item.coupon_lines,
|
||||
fulfillments,
|
||||
raw: item,
|
||||
};
|
||||
}
|
||||
|
||||
// 订单操作方法
|
||||
|
|
@ -595,7 +609,7 @@ export class WooCommerceAdapter implements ISiteAdapter {
|
|||
// await api.put(`orders/${orderId}`, { status: 'processing' });
|
||||
|
||||
// // 添加取消履行的备注
|
||||
// const note = `订单履行已取消${data.reason ? `,原因:${data.reason}` : ''}`;
|
||||
// const note = `订单履行已取消${data.reason ? `,原因:${data.reason}` : ''}`;
|
||||
// await api.post(`orders/${orderId}/notes`, { note, customer_note: true });
|
||||
|
||||
// return {
|
||||
|
|
@ -684,14 +698,14 @@ export class WooCommerceAdapter implements ISiteAdapter {
|
|||
}));
|
||||
}
|
||||
|
||||
// 映射变体数据(注意:WooCommerce API 中变体通常通过单独的端点处理)
|
||||
// 映射变体数据(注意:WooCommerce API 中变体通常通过单独的端点处理)
|
||||
// 这里只映射变体的基本信息,具体创建/更新变体需要额外处理
|
||||
if (data.variations && Array.isArray(data.variations)) {
|
||||
// 对于WooProduct类型,variations字段只存储变体ID
|
||||
mapped.variations = data.variations.map(variation => variation.id as number);
|
||||
}
|
||||
|
||||
// 映射下载数据(如果产品是可下载的)
|
||||
// 映射下载数据(如果产品是可下载的)
|
||||
// if (data.downloads && Array.isArray(data.downloads)) {
|
||||
// mapped.downloads = data.downloads.map(download => ({
|
||||
// id: download.id as number,
|
||||
|
|
|
|||
|
|
@ -7,11 +7,8 @@ export default {
|
|||
// dataSource: {
|
||||
// default: {
|
||||
// host: '13.212.62.127',
|
||||
// port: "3306",
|
||||
// username: 'root',
|
||||
// password: 'Yoone!@.2025',
|
||||
// database: 'inventory_v2',
|
||||
// synchronize: true,
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
|
|
@ -22,7 +19,7 @@ export default {
|
|||
port: "3306",
|
||||
username: 'root',
|
||||
password: 'root',
|
||||
database: 'inventory'
|
||||
database: 'inventory',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -99,7 +99,7 @@ export class MainConfiguration {
|
|||
}
|
||||
|
||||
/**
|
||||
* 初始化数据库(如果不存在则创建)
|
||||
* 初始化数据库(如果不存在则创建)
|
||||
*/
|
||||
private async initializeDatabase(): Promise<void> {
|
||||
// 使用注入的数据库配置
|
||||
|
|
|
|||
|
|
@ -299,7 +299,7 @@ export class SiteApiController {
|
|||
}
|
||||
}
|
||||
|
||||
// 平台特性:产品导出(特殊CSV,走平台服务)
|
||||
// 平台特性:产品导出(特殊CSV,走平台服务)
|
||||
@Get('/:siteId/links')
|
||||
async getLinks(
|
||||
@Param('siteId') siteId: number
|
||||
|
|
@ -429,7 +429,7 @@ export class SiteApiController {
|
|||
}
|
||||
}
|
||||
|
||||
// 平台特性:产品导入(特殊CSV,走平台服务)
|
||||
// 平台特性:产品导入(特殊CSV,走平台服务)
|
||||
@Post('/:siteId/products/import-special')
|
||||
@ApiOkResponse({ type: Object })
|
||||
async importProductsSpecial(
|
||||
|
|
@ -443,7 +443,7 @@ export class SiteApiController {
|
|||
const created: any[] = [];
|
||||
const failed: any[] = [];
|
||||
if (site.type === 'woocommerce') {
|
||||
// 解析 CSV 为对象数组(若传入 items 则优先 items)
|
||||
// 解析 CSV 为对象数组(若传入 items 则优先 items)
|
||||
let payloads = items;
|
||||
if (!payloads.length && csvText) {
|
||||
const lines = csvText.split(/\r?\n/).filter(Boolean);
|
||||
|
|
|
|||
|
|
@ -9,10 +9,13 @@ import {
|
|||
} from '@midwayjs/decorator';
|
||||
import { Context } from '@midwayjs/koa';
|
||||
import * as crypto from 'crypto';
|
||||
|
||||
|
||||
import { SiteService } from '../service/site.service';
|
||||
import { OrderService } from '../service/order.service';
|
||||
import { SiteApiService } from '../service/site-api.service';
|
||||
|
||||
import {
|
||||
UnifiedOrderDTO,
|
||||
} from '../dto/site-api.dto';
|
||||
|
||||
@Controller('/webhook')
|
||||
export class WebhookController {
|
||||
|
|
@ -28,11 +31,9 @@ export class WebhookController {
|
|||
|
||||
@Logger()
|
||||
logger: ILogger;
|
||||
|
||||
|
||||
@Inject()
|
||||
private readonly siteService: SiteService;
|
||||
@Inject()
|
||||
private readonly siteApiService: SiteApiService;
|
||||
|
||||
// 移除配置中的站点数组,来源统一改为数据库
|
||||
|
||||
|
|
@ -48,7 +49,7 @@ export class WebhookController {
|
|||
@Query('siteId') siteIdStr: string,
|
||||
@Headers() header: any
|
||||
) {
|
||||
console.log(`webhook woocommerce`, siteIdStr, body, header)
|
||||
console.log(`webhook woocommerce`, siteIdStr, body,header)
|
||||
const signature = header['x-wc-webhook-signature'];
|
||||
const topic = header['x-wc-webhook-topic'];
|
||||
const source = header['x-wc-webhook-source'];
|
||||
|
|
@ -78,44 +79,43 @@ export class WebhookController {
|
|||
.update(rawBody)
|
||||
.digest('base64');
|
||||
try {
|
||||
if (hash !== signature) {
|
||||
if (hash === signature) {
|
||||
switch (topic) {
|
||||
case 'product.created':
|
||||
case 'product.updated':
|
||||
// 不再写入本地,平台事件仅确认接收
|
||||
break;
|
||||
case 'product.deleted':
|
||||
// 不再写入本地,平台事件仅确认接收
|
||||
break;
|
||||
case 'order.created':
|
||||
case 'order.updated':
|
||||
await this.orderService.syncSingleOrder(siteId, body);
|
||||
break;
|
||||
case 'order.deleted':
|
||||
break;
|
||||
case 'customer.created':
|
||||
break;
|
||||
case 'customer.updated':
|
||||
break;
|
||||
case 'customer.deleted':
|
||||
break;
|
||||
default:
|
||||
console.log('Unhandled event:', body.event);
|
||||
}
|
||||
|
||||
return {
|
||||
code: 200,
|
||||
success: true,
|
||||
message: 'Webhook processed successfully',
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
code: 403,
|
||||
success: false,
|
||||
message: 'Webhook verification failed',
|
||||
};
|
||||
}
|
||||
const adapter = await this.siteApiService.getAdapter(siteId);
|
||||
switch (topic) {
|
||||
case 'product.created':
|
||||
case 'product.updated':
|
||||
// 不再写入本地,平台事件仅确认接收
|
||||
break;
|
||||
case 'product.deleted':
|
||||
// 不再写入本地,平台事件仅确认接收
|
||||
break;
|
||||
case 'order.created':
|
||||
case 'order.updated':
|
||||
const order = adapter.mapPlatformToUnifiedOrder(body)
|
||||
await this.orderService.syncSingleOrder(siteId, order);
|
||||
break;
|
||||
case 'order.deleted':
|
||||
break;
|
||||
case 'customer.created':
|
||||
break;
|
||||
case 'customer.updated':
|
||||
break;
|
||||
case 'customer.deleted':
|
||||
break;
|
||||
default:
|
||||
console.log('Unhandled event:', body.event);
|
||||
|
||||
return {
|
||||
code: 200,
|
||||
success: true,
|
||||
message: 'Webhook processed successfully',
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
|
|
@ -130,10 +130,23 @@ export class WebhookController {
|
|||
@Query('signature') signature: string,
|
||||
@Headers() header: any
|
||||
) {
|
||||
console.log(`webhook shoppy`, siteIdStr, body, header)
|
||||
const topic = header['x-oemsaas-event-type'];
|
||||
// const source = header['x-oemsaas-shop-domain'];
|
||||
// const source = header['x-oemsaas-shop-domain'];
|
||||
const siteId = Number(siteIdStr);
|
||||
const bodys = new UnifiedOrderDTO();
|
||||
Object.assign(bodys, body);
|
||||
// 从数据库获取站点配置
|
||||
const site = await this.siteService.get(siteId, true);
|
||||
|
||||
// if (!site || !source?.includes(site.websiteUrl)) {
|
||||
if (!site) {
|
||||
console.log('domain not match');
|
||||
return {
|
||||
code: HttpStatus.BAD_REQUEST,
|
||||
success: false,
|
||||
message: 'domain not match',
|
||||
};
|
||||
}
|
||||
|
||||
if (!signature) {
|
||||
return {
|
||||
|
|
@ -149,7 +162,6 @@ export class WebhookController {
|
|||
// .createHmac('sha256', this.secret)
|
||||
// .update(rawBody)
|
||||
// .digest('base64');
|
||||
const adapter = await this.siteApiService.getAdapter(siteId);
|
||||
try {
|
||||
if (this.secret === signature) {
|
||||
switch (topic) {
|
||||
|
|
@ -162,8 +174,7 @@ export class WebhookController {
|
|||
break;
|
||||
case 'orders/create':
|
||||
case 'orders/update':
|
||||
const order = adapter.mapPlatformToUnifiedOrder(body)
|
||||
await this.orderService.syncSingleOrder(siteId, order);
|
||||
await this.orderService.syncSingleOrder(siteId, bodys);
|
||||
break;
|
||||
case 'orders/delete':
|
||||
break;
|
||||
|
|
|
|||
|
|
@ -126,7 +126,7 @@ const flavorsData = [
|
|||
{ name: 'arctic-mint', title: 'arctic mint', titleCn: '北极薄荷', shortName: 'AR' },
|
||||
{ name: 'baddie-blueberries', title: 'baddie blueberries', titleCn: '时髦蓝莓', shortName: 'BA' },
|
||||
{ name: 'banana', title: 'banana', titleCn: '香蕉', shortName: 'BA' },
|
||||
{ name: 'banana-(solid)', title: 'banana (solid)', titleCn: '香蕉(固体)', shortName: 'BA' },
|
||||
{ name: 'banana-(solid)', title: 'banana (solid)', titleCn: '香蕉(固体)', shortName: 'BA' },
|
||||
{ name: 'banana-berry', title: 'banana berry', titleCn: '香蕉莓果', shortName: 'BA' },
|
||||
{ name: 'banana-berry-melon-ice', title: 'banana berry melon ice', titleCn: '香蕉莓果瓜冰', shortName: 'BA' },
|
||||
{ name: 'banana-blackberry', title: 'banana blackberry', titleCn: '香蕉黑莓', shortName: 'BA' },
|
||||
|
|
@ -137,7 +137,7 @@ const flavorsData = [
|
|||
{ name: 'bangin-blood-orange-iced', title: 'bangin blood orange iced', titleCn: '爆炸血橙冰', shortName: 'BA' },
|
||||
{ name: 'berries-in-the-6ix', title: 'berries in the 6ix', titleCn: '多伦多莓果', shortName: 'BE' },
|
||||
{ name: 'berry-burst', title: 'berry burst', titleCn: '浆果爆发', shortName: 'BE' },
|
||||
{ name: 'berry-burst-(thermal)', title: 'berry burst (thermal)', titleCn: '浆果爆发(热感)', shortName: 'BE' },
|
||||
{ name: 'berry-burst-(thermal)', title: 'berry burst (thermal)', titleCn: '浆果爆发(热感)', shortName: 'BE' },
|
||||
{ name: 'berry-ice', title: 'berry ice', titleCn: '浆果冰', shortName: 'BE' },
|
||||
{ name: 'berry-lime-ice', title: 'berry lime ice', titleCn: '浆果青柠冰', shortName: 'BE' },
|
||||
{ name: 'berry-trio-ice', title: 'berry trio ice', titleCn: '三重浆果冰', shortName: 'BE' },
|
||||
|
|
@ -145,7 +145,7 @@ const flavorsData = [
|
|||
{ name: 'black-cherry', title: 'black cherry', titleCn: '黑樱桃', shortName: 'BL' },
|
||||
{ name: 'blackcherry', title: 'blackcherry', titleCn: '黑樱桃混合', shortName: 'BL' },
|
||||
{ name: 'blackcurrant-ice', title: 'blackcurrant ice', titleCn: '黑加仑冰', shortName: 'BL' },
|
||||
{ name: 'black-currant-ice', title: 'black currant ice', titleCn: '黑加仑冰(空格版)', shortName: 'BL' },
|
||||
{ name: 'black-currant-ice', title: 'black currant ice', titleCn: '黑加仑冰(空格版)', shortName: 'BL' },
|
||||
{ name: 'black-licorice', title: 'black licorice', titleCn: '黑甘草', shortName: 'BL' },
|
||||
{ name: 'black-tea', title: 'black tea', titleCn: '红茶', shortName: 'BL' },
|
||||
{ name: 'blackberry-ice', title: 'blackberry ice', titleCn: '黑莓冰', shortName: 'BL' },
|
||||
|
|
@ -168,7 +168,7 @@ const flavorsData = [
|
|||
{ name: 'blue-razz', title: 'blue razz', titleCn: '蓝覆盆子', shortName: 'BL' },
|
||||
{ name: 'blue-razz-hype', title: 'blue razz hype', titleCn: '蓝覆盆子热情', shortName: 'BL' },
|
||||
{ name: 'blue-razz-ice', title: 'blue razz ice', titleCn: '蓝覆盆子冰', shortName: 'BL' },
|
||||
{ name: 'blue-razz-ice-(solid)', title: 'blue razz ice (solid)', titleCn: '蓝覆盆子冰(固体)', shortName: 'BL' },
|
||||
{ name: 'blue-razz-ice-(solid)', title: 'blue razz ice (solid)', titleCn: '蓝覆盆子冰(固体)', shortName: 'BL' },
|
||||
{ name: 'blue-razz-ice-glace', title: 'blue razz ice glace', titleCn: '蓝覆盆子冰格', shortName: 'BL' },
|
||||
{ name: 'blue-razz-lemon-ice', title: 'blue razz lemon ice', titleCn: '蓝覆盆子柠檬冰', shortName: 'BL' },
|
||||
{ name: 'blue-razz-lemonade', title: 'blue razz lemonade', titleCn: '蓝覆盆子柠檬水', shortName: 'BL' },
|
||||
|
|
@ -196,7 +196,7 @@ const flavorsData = [
|
|||
{ name: 'bumpin-blackcurrant-iced', title: 'bumpin blackcurrant iced', titleCn: '黑加仑热烈冰', shortName: 'BU' },
|
||||
{ name: 'burst-ice', title: 'burst ice', titleCn: '爆炸冰', shortName: 'BU' },
|
||||
{ name: 'bussin-banana-iced', title: 'bussin banana iced', titleCn: '香蕉热烈冰', shortName: 'BU' },
|
||||
{ name: 'bussin-banana-iced', title: 'bussin banana iced', titleCn: '香蕉热烈冰(重复)', shortName: 'BU' },
|
||||
{ name: 'bussin-banana-iced', title: 'bussin banana iced', titleCn: '香蕉热烈冰(重复)', shortName: 'BU' },
|
||||
{ name: 'california-cherry', title: 'california cherry', titleCn: '加州樱桃', shortName: 'CA' },
|
||||
{ name: 'cantaloupe-mango-banana', title: 'cantaloupe mango banana', titleCn: '香瓜芒果香蕉', shortName: 'CA' },
|
||||
{ name: 'caramel', title: 'caramel', titleCn: '焦糖', shortName: 'CA' },
|
||||
|
|
@ -230,7 +230,7 @@ const flavorsData = [
|
|||
{ name: 'citrus-chill', title: 'citrus chill', titleCn: '柑橘清凉', shortName: 'CI' },
|
||||
{ name: 'citrus-smash-ice', title: 'citrus smash ice', titleCn: '柑橘冲击冰', shortName: 'CI' },
|
||||
{ name: 'citrus-sunrise', title: 'citrus sunrise', titleCn: '柑橘日出', shortName: 'CI' },
|
||||
{ name: 'citrus-sunrise-(thermal)', title: 'citrus sunrise (thermal)', titleCn: '柑橘日出(热感)', shortName: 'CI' },
|
||||
{ name: 'citrus-sunrise-(thermal)', title: 'citrus sunrise (thermal)', titleCn: '柑橘日出(热感)', shortName: 'CI' },
|
||||
{ name: 'classic', title: 'classic', titleCn: '经典', shortName: 'CL' },
|
||||
{ name: 'classic-ice', title: 'classic ice', titleCn: '经典冰', shortName: 'CL' },
|
||||
{ name: 'classic-mint-ice', title: 'classic mint ice', titleCn: '经典薄荷冰', shortName: 'CL' },
|
||||
|
|
@ -310,7 +310,7 @@ const flavorsData = [
|
|||
{ name: 'fizzy', title: 'fizzy', titleCn: '汽水', shortName: 'FI' },
|
||||
{ name: 'flavourless', title: 'flavourless', titleCn: '无味', shortName: 'FL' },
|
||||
{ name: 'flippin-fruit-flash', title: 'flippin fruit flash', titleCn: '翻转水果闪电', shortName: 'FL' },
|
||||
{ name: 'flippin-fruit-flash-(rainbow-burst)', title: 'flippin fruit flash (rainbow burst)', titleCn: '翻转水果闪电(彩虹爆发)', shortName: 'FL' },
|
||||
{ name: 'flippin-fruit-flash-(rainbow-burst)', title: 'flippin fruit flash (rainbow burst)', titleCn: '翻转水果闪电(彩虹爆发)', shortName: 'FL' },
|
||||
{ name: 'forest-fruits', title: 'forest fruits', titleCn: '森林水果', shortName: 'FO' },
|
||||
{ name: 'fragrant-grapefruit', title: 'fragrant grapefruit', titleCn: '香气葡萄柚', shortName: 'FR' },
|
||||
{ name: 'freeze', title: 'freeze', titleCn: '冰冻', shortName: 'FR' },
|
||||
|
|
@ -340,14 +340,14 @@ const flavorsData = [
|
|||
{ name: 'fuji-melon-ice', title: 'fuji melon ice', titleCn: '富士瓜冰', shortName: 'FU' },
|
||||
{ name: 'full-charge', title: 'full charge', titleCn: '满电', shortName: 'FU' },
|
||||
{ name: 'gb', title: 'gb', titleCn: '软糖', shortName: 'GB' },
|
||||
{ name: 'gb(gummy-bear)', title: 'gb(gummy bear)', titleCn: '软糖(Gummy Bear)', shortName: 'GB' },
|
||||
{ name: 'gb(gummy-bear)', title: 'gb(gummy bear)', titleCn: '软糖(Gummy Bear)', shortName: 'GB' },
|
||||
{ name: 'gentle-mint', title: 'gentle mint', titleCn: '温和薄荷', shortName: 'GE' },
|
||||
{ name: 'ghost-cola-&-vanilla', title: 'ghost cola & vanilla', titleCn: '幽灵可乐香草', shortName: 'GH' },
|
||||
{ name: 'ghost-cola-ice', title: 'ghost cola ice', titleCn: '幽灵可乐冰', shortName: 'GH' },
|
||||
{ name: 'ghost-mango', title: 'ghost mango', titleCn: '幽灵芒果', shortName: 'GH' },
|
||||
{ name: 'ghost-original', title: 'ghost original', titleCn: '幽灵原味', shortName: 'GH' },
|
||||
{ name: 'ghost-watermelon-ice', title: 'ghost watermelon ice', titleCn: '幽灵西瓜冰', shortName: 'GH' },
|
||||
{ name: 'gnarly-green-d-(green-dew)', title: 'gnarly green d (green dew)', titleCn: '狂野绿 D(绿色露水)', shortName: 'GN' },
|
||||
{ name: 'gnarly-green-d-(green-dew)', title: 'gnarly green d (green dew)', titleCn: '狂野绿 D(绿色露水)', shortName: 'GN' },
|
||||
{ name: 'gold-edition', title: 'gold edition', titleCn: '金版', shortName: 'GO' },
|
||||
{ name: 'grape', title: 'grape', titleCn: '葡萄', shortName: 'GR' },
|
||||
{ name: 'grape-cherry', title: 'grape cherry', titleCn: '葡萄樱桃', shortName: 'GR' },
|
||||
|
|
@ -492,13 +492,13 @@ const flavorsData = [
|
|||
{ name: 'mixed-fruit', title: 'mixed fruit', titleCn: '混合水果', shortName: 'MI' },
|
||||
{ name: 'mocha-ice', title: 'mocha ice', titleCn: '摩卡冰', shortName: 'MO' },
|
||||
{ name: 'morocco-mint', title: 'morocco mint', titleCn: '摩洛哥薄荷', shortName: 'MO' },
|
||||
{ name: 'morocco-mint-(thermal)', title: 'morocco mint (thermal)', titleCn: '摩洛哥薄荷(热感)', shortName: 'MO' },
|
||||
{ name: 'morocco-mint-(thermal)', title: 'morocco mint (thermal)', titleCn: '摩洛哥薄荷(热感)', shortName: 'MO' },
|
||||
{ name: 'mung-beans', title: 'mung beans', titleCn: '绿豆', shortName: 'MU' },
|
||||
{ name: 'nasty-tropic', title: 'nasty tropic', titleCn: '恶搞热带', shortName: 'NA' },
|
||||
{ name: 'nectarine-ice', title: 'nectarine ice', titleCn: '油桃冰', shortName: 'NE' },
|
||||
{ name: 'night-rider', title: 'night rider', titleCn: '夜骑', shortName: 'NI' },
|
||||
{ name: 'nirvana', title: 'nirvana', titleCn: '宁静蓝莓', shortName: 'NI' },
|
||||
{ name: 'north-american-style(root-beer)', title: 'north american style(root beer)', titleCn: '北美风格(根啤)', shortName: 'NO' },
|
||||
{ name: 'north-american-style(root-beer)', title: 'north american style(root beer)', titleCn: '北美风格(根啤)', shortName: 'NO' },
|
||||
{ name: 'northern-blue-razz', title: 'northern blue razz', titleCn: '北方蓝覆盆子', shortName: 'NO' },
|
||||
{ name: 'nutty-virginia', title: 'nutty virginia', titleCn: '坚果弗吉尼亚', shortName: 'NU' },
|
||||
{ name: 'orange', title: 'orange', titleCn: '橙子', shortName: 'OR' },
|
||||
|
|
@ -508,12 +508,12 @@ const flavorsData = [
|
|||
{ name: 'orange-mango-guava', title: 'orange mango guava', titleCn: '橙子芒果番石榴', shortName: 'OR' },
|
||||
{ name: 'orange-mango-pineapple-ice', title: 'orange mango pineapple ice', titleCn: '橙子芒果菠萝冰', shortName: 'OR' },
|
||||
{ name: 'orange-p', title: 'orange p', titleCn: '橙子 P', shortName: 'OR' },
|
||||
{ name: 'orange-p(fanta)', title: 'orange p(fanta)', titleCn: '橙子 P(芬达)', shortName: 'OR' },
|
||||
{ name: 'orange-p(fanta)', title: 'orange p(fanta)', titleCn: '橙子 P(芬达)', shortName: 'OR' },
|
||||
{ name: 'orange-spark', title: 'orange spark', titleCn: '橙色火花', shortName: 'OR' },
|
||||
{ name: 'orange-tangerine', title: 'orange tangerine', titleCn: '橙子柑橘', shortName: 'OR' },
|
||||
{ name: 'original', title: 'original', titleCn: '原味', shortName: 'OR' },
|
||||
{ name: 'packin-peach-berry', title: 'packin peach berry', titleCn: '装满桃浆果', shortName: 'PA' },
|
||||
{ name: 'packin-peach-berry-(popn-peach-berry)', title: 'packin peach berry (popn peach berry)', titleCn: '装满桃浆果(Pop’n 桃浆果)', shortName: 'PA' },
|
||||
{ name: 'packin-peach-berry-(popn-peach-berry)', title: 'packin peach berry (popn peach berry)', titleCn: '装满桃浆果(Pop’n 桃浆果)', shortName: 'PA' },
|
||||
{ name: 'papio', title: 'papio', titleCn: 'Papio', shortName: 'PA' },
|
||||
{ name: 'paradise', title: 'paradise', titleCn: '天堂', shortName: 'PA' },
|
||||
{ name: 'paradise-iced', title: 'paradise iced', titleCn: '天堂冰', shortName: 'PA' },
|
||||
|
|
@ -603,7 +603,7 @@ const flavorsData = [
|
|||
{ name: 'red-fruits', title: 'red fruits', titleCn: '红色水果', shortName: 'RE' },
|
||||
{ name: 'red-lightning', title: 'red lightning', titleCn: '红色闪电', shortName: 'RE' },
|
||||
{ name: 'red-line', title: 'red line', titleCn: '红线', shortName: 'RE' },
|
||||
{ name: 'red-line-(energy-drink)', title: 'red line (energy drink)', titleCn: '红线(能量饮料)', shortName: 'RE' },
|
||||
{ name: 'red-line-(energy-drink)', title: 'red line (energy drink)', titleCn: '红线(能量饮料)', shortName: 'RE' },
|
||||
{ name: 'red-magic', title: 'red magic', titleCn: '红魔', shortName: 'RE' },
|
||||
{ name: 'rich-tobacco', title: 'rich tobacco', titleCn: '浓烈烟草', shortName: 'RI' },
|
||||
{ name: 'root-beer', title: 'root beer', titleCn: '根啤', shortName: 'RO' },
|
||||
|
|
@ -625,8 +625,8 @@ const flavorsData = [
|
|||
{ name: 'sic-strawberry-iced', title: 'sic strawberry iced', titleCn: '意大利草莓冰', shortName: 'SI' },
|
||||
{ name: 'simply-spearmint', title: 'simply spearmint', titleCn: '清爽留兰香', shortName: 'SI' },
|
||||
{ name: 'skc', title: 'skc', titleCn: 'SKC', shortName: 'SK' },
|
||||
{ name: 'skc(skittles-candy)', title: 'skc(skittles candy)', titleCn: 'SKC(彩虹糖)', shortName: 'SK' },
|
||||
{ name: 'slammin-sts-(sour-snap)', title: 'slammin sts (sour snap)', titleCn: '热烈 STS(酸糖)', shortName: 'SL' },
|
||||
{ name: 'skc(skittles-candy)', title: 'skc(skittles candy)', titleCn: 'SKC(彩虹糖)', shortName: 'SK' },
|
||||
{ name: 'slammin-sts-(sour-snap)', title: 'slammin sts (sour snap)', titleCn: '热烈 STS(酸糖)', shortName: 'SL' },
|
||||
{ name: 'slammin-sts-iced', title: 'slammin sts iced', titleCn: '热烈 STS 冰', shortName: 'SL' },
|
||||
{ name: 'smooth', title: 'smooth', titleCn: '顺滑', shortName: 'SM' },
|
||||
{ name: 'smooth-mint', title: 'smooth mint', titleCn: '顺滑薄荷', shortName: 'SM' },
|
||||
|
|
@ -664,7 +664,7 @@ const flavorsData = [
|
|||
{ name: 'strawberry-jasmine-t', title: 'strawberry jasmine t', titleCn: '草莓茉莉茶', shortName: 'ST' },
|
||||
{ name: 'strawberry-jasmine-tea', title: 'strawberry jasmine tea', titleCn: '草莓茉莉茶', shortName: 'ST' },
|
||||
{ name: 'strawberry-kiwi', title: 'strawberry kiwi', titleCn: '草莓奇异果', shortName: 'ST' },
|
||||
{ name: 'strawberry-kiwi-(solid)', title: 'strawberry kiwi (solid)', titleCn: '草莓奇异果(固体)', shortName: 'ST' },
|
||||
{ name: 'strawberry-kiwi-(solid)', title: 'strawberry kiwi (solid)', titleCn: '草莓奇异果(固体)', shortName: 'ST' },
|
||||
{ name: 'strawberry-kiwi-banana-ice', title: 'strawberry kiwi banana ice', titleCn: '草莓奇异果香蕉冰', shortName: 'ST' },
|
||||
{ name: 'strawberry-kiwi-guava-ice', title: 'strawberry kiwi guava ice', titleCn: '草莓奇异果番石榴冰', shortName: 'ST' },
|
||||
{ name: 'strawberry-kiwi-ice', title: 'strawberry kiwi ice', titleCn: '草莓奇异果冰', shortName: 'ST' },
|
||||
|
|
@ -680,10 +680,10 @@ const flavorsData = [
|
|||
{ name: 'strawberry-watermelon', title: 'strawberry watermelon', titleCn: '草莓西瓜', shortName: 'ST' },
|
||||
{ name: 'strawberry-watermelon-ice', title: 'strawberry watermelon ice', titleCn: '草莓西瓜冰', shortName: 'ST' },
|
||||
{ name: 'strawmelon-peach', title: 'strawmelon peach', titleCn: '草莓桃', shortName: 'ST' },
|
||||
{ name: 'strawmelon-peach-(solid)', title: 'strawmelon peach (solid)', titleCn: '草莓桃(固体)', shortName: 'ST' },
|
||||
{ name: 'strawmelon-peach-(solid)', title: 'strawmelon peach (solid)', titleCn: '草莓桃(固体)', shortName: 'ST' },
|
||||
{ name: 'strawnana-orange', title: 'strawnana orange', titleCn: '草莓香蕉橙', shortName: 'ST' },
|
||||
{ name: 'summer-grape', title: 'summer grape', titleCn: '夏日葡萄', shortName: 'SU' },
|
||||
{ name: 'summer-grape-(thermal)', title: 'summer grape (thermal)', titleCn: '夏日葡萄(热感)', shortName: 'SU' },
|
||||
{ name: 'summer-grape-(thermal)', title: 'summer grape (thermal)', titleCn: '夏日葡萄(热感)', shortName: 'SU' },
|
||||
{ name: 'super-sour-blueberry-iced', title: 'super sour blueberry iced', titleCn: '超级酸蓝莓冰', shortName: 'SU' },
|
||||
{ name: 'super-spearmint', title: 'super spearmint', titleCn: '超级留兰香', shortName: 'SU' },
|
||||
{ name: 'super-spearmint-iced', title: 'super spearmint iced', titleCn: '超级留兰香冰', shortName: 'SU' },
|
||||
|
|
@ -704,7 +704,7 @@ const flavorsData = [
|
|||
{ name: 'tropical-orang-ice', title: 'tropical orang ice', titleCn: '热带橙冰', shortName: 'TR' },
|
||||
{ name: 'tropical-prism-blast', title: 'tropical prism blast', titleCn: '热带棱镜爆炸', shortName: 'TR' },
|
||||
{ name: 'tropical-splash', title: 'tropical splash', titleCn: '热带飞溅', shortName: 'TR' },
|
||||
{ name: 'tropical-splash-(solid)', title: 'tropical splash (solid)', titleCn: '热带飞溅(固体)', shortName: 'TR' },
|
||||
{ name: 'tropical-splash-(solid)', title: 'tropical splash (solid)', titleCn: '热带飞溅(固体)', shortName: 'TR' },
|
||||
{ name: 'tropical-storm-ice', title: 'tropical storm ice', titleCn: '热带风暴冰', shortName: 'TR' },
|
||||
{ name: 'tropical-summer', title: 'tropical summer', titleCn: '热带夏日', shortName: 'TR' },
|
||||
{ name: 'tropika', title: 'tropika', titleCn: '热带果', shortName: 'TR' },
|
||||
|
|
@ -728,7 +728,7 @@ const flavorsData = [
|
|||
{ name: 'watermelon-cantaloupe-honeydew-ice', title: 'watermelon cantaloupe honeydew ice', titleCn: '西瓜香瓜蜜瓜冰', shortName: 'WA' },
|
||||
{ name: 'watermelon-g', title: 'watermelon g', titleCn: '西瓜 G', shortName: 'WA' },
|
||||
{ name: 'watermelon-ice', title: 'watermelon ice', titleCn: '西瓜冰', shortName: 'WA' },
|
||||
{ name: 'watermelon-ice-(solid)', title: 'watermelon ice (solid)', titleCn: '西瓜冰(固体)', shortName: 'WA' },
|
||||
{ name: 'watermelon-ice-(solid)', title: 'watermelon ice (solid)', titleCn: '西瓜冰(固体)', shortName: 'WA' },
|
||||
{ name: 'watermelon-lime-ice', title: 'watermelon lime ice', titleCn: '西瓜青柠冰', shortName: 'WA' },
|
||||
{ name: 'watermelon-mango-tango', title: 'watermelon mango tango', titleCn: '西瓜芒果探戈', shortName: 'WA' },
|
||||
{ name: 'watermelona-cg', title: 'watermelona cg', titleCn: '西瓜 CG', shortName: 'WA' },
|
||||
|
|
@ -750,7 +750,7 @@ const flavorsData = [
|
|||
{ name: 'wild-strawberry-watermelon', title: 'wild strawberry watermelon', titleCn: '野生草莓西瓜', shortName: 'WI' },
|
||||
{ name: 'wild-white-grape', title: 'wild white grape', titleCn: '野生白葡萄', shortName: 'WI' },
|
||||
{ name: 'wild-white-grape-ice', title: 'wild white grape ice', titleCn: '野生白葡萄冰', shortName: 'WI' },
|
||||
{ name: 'wild-white-grape-iced', title: 'wild white grape iced', titleCn: '野生白葡萄冰(冷饮)', shortName: 'WI' },
|
||||
{ name: 'wild-white-grape-iced', title: 'wild white grape iced', titleCn: '野生白葡萄冰(冷饮)', shortName: 'WI' },
|
||||
{ name: 'winter-berry-ice', title: 'winter berry ice', titleCn: '冬季浆果冰', shortName: 'WI' },
|
||||
{ name: 'winter-green', title: 'winter green', titleCn: '冬青', shortName: 'WI' },
|
||||
{ name: 'wintergreen', title: 'wintergreen', titleCn: '冬青薄荷', shortName: 'WI' },
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@ export class UnifiedSearchParamsDTO<Where=Record<string, any>> {
|
|||
* 批量操作错误项
|
||||
*/
|
||||
export interface BatchErrorItem {
|
||||
// 错误项标识(可以是ID、邮箱等)
|
||||
// 错误项标识(可以是ID、邮箱等)
|
||||
identifier: string;
|
||||
// 错误信息
|
||||
error: string;
|
||||
|
|
@ -76,7 +76,7 @@ export interface BatchOperationResult {
|
|||
updated?: number;
|
||||
// 删除数量
|
||||
deleted?: number;
|
||||
// 跳过的数量(如数据已存在或无需处理)
|
||||
// 跳过的数量(如数据已存在或无需处理)
|
||||
skipped?: number;
|
||||
// 错误列表
|
||||
errors: BatchErrorItem[];
|
||||
|
|
@ -101,7 +101,7 @@ export class SyncOperationResult implements BatchOperationResult {
|
|||
* 批量操作错误项DTO
|
||||
*/
|
||||
export class BatchErrorItemDTO {
|
||||
@ApiProperty({ description: '错误项标识(如ID、邮箱等)', type: String })
|
||||
@ApiProperty({ description: '错误项标识(如ID、邮箱等)', type: String })
|
||||
@Rule(RuleType.string().required())
|
||||
identifier: string;
|
||||
|
||||
|
|
@ -164,7 +164,7 @@ export class SyncParamsDTO {
|
|||
@Rule(RuleType.string().optional())
|
||||
endDate?: string;
|
||||
|
||||
@ApiProperty({ description: '强制同步(忽略缓存)', type: Boolean, required: false, default: false })
|
||||
@ApiProperty({ description: '强制同步(忽略缓存)', type: Boolean, required: false, default: false })
|
||||
@Rule(RuleType.boolean().optional())
|
||||
force?: boolean = false;
|
||||
}
|
||||
|
|
@ -183,7 +183,7 @@ export class BatchQueryDTO {
|
|||
}
|
||||
|
||||
/**
|
||||
* 批量操作结果类(泛型支持)
|
||||
* 批量操作结果类(泛型支持)
|
||||
*/
|
||||
export class BatchOperationResultDTOGeneric<T> extends BatchOperationResultDTO {
|
||||
@ApiProperty({ description: '操作成功的数据列表', type: Array })
|
||||
|
|
@ -191,7 +191,7 @@ export class BatchOperationResultDTOGeneric<T> extends BatchOperationResultDTO {
|
|||
}
|
||||
|
||||
/**
|
||||
* 同步操作结果类(泛型支持)
|
||||
* 同步操作结果类(泛型支持)
|
||||
*/
|
||||
export class SyncOperationResultDTOGeneric<T> extends SyncOperationResultDTO {
|
||||
@ApiProperty({ description: '同步成功的数据列表', type: Array })
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import { Rule, RuleType } from '@midwayjs/validate';
|
|||
* 批量操作错误项
|
||||
*/
|
||||
export interface BatchErrorItem {
|
||||
// 错误项标识(可以是ID、邮箱等)
|
||||
// 错误项标识(可以是ID、邮箱等)
|
||||
identifier: string;
|
||||
// 错误信息
|
||||
error: string;
|
||||
|
|
@ -25,7 +25,7 @@ export interface BatchOperationResult {
|
|||
updated?: number;
|
||||
// 删除数量
|
||||
deleted?: number;
|
||||
// 跳过的数量(如数据已存在或无需处理)
|
||||
// 跳过的数量(如数据已存在或无需处理)
|
||||
skipped?: number;
|
||||
// 错误列表
|
||||
errors: BatchErrorItem[];
|
||||
|
|
@ -43,7 +43,7 @@ export interface SyncOperationResult extends BatchOperationResult {
|
|||
* 批量操作错误项DTO
|
||||
*/
|
||||
export class BatchErrorItemDTO {
|
||||
@ApiProperty({ description: '错误项标识(如ID、邮箱等)', type: String })
|
||||
@ApiProperty({ description: '错误项标识(如ID、邮箱等)', type: String })
|
||||
@Rule(RuleType.string().required())
|
||||
identifier: string;
|
||||
|
||||
|
|
@ -114,7 +114,7 @@ export class BatchDeleteDTO {
|
|||
}
|
||||
|
||||
/**
|
||||
* 批量操作请求DTO(包含增删改)
|
||||
* 批量操作请求DTO(包含增删改)
|
||||
*/
|
||||
export class BatchOperationDTO<T = any> {
|
||||
@ApiProperty({ description: '要创建的数据列表', type: Array, required: false })
|
||||
|
|
@ -175,7 +175,7 @@ export class SyncParamsDTO {
|
|||
@Rule(RuleType.string().optional())
|
||||
endDate?: string;
|
||||
|
||||
@ApiProperty({ description: '强制同步(忽略缓存)', type: Boolean, required: false, default: false })
|
||||
@ApiProperty({ description: '强制同步(忽略缓存)', type: Boolean, required: false, default: false })
|
||||
@Rule(RuleType.boolean().optional())
|
||||
force?: boolean = false;
|
||||
}
|
||||
|
|
@ -194,7 +194,7 @@ export class BatchQueryDTO {
|
|||
}
|
||||
|
||||
/**
|
||||
* 批量操作结果类(泛型支持)
|
||||
* 批量操作结果类(泛型支持)
|
||||
*/
|
||||
export class BatchOperationResultDTOGeneric<T> extends BatchOperationResultDTO {
|
||||
@ApiProperty({ description: '操作成功的数据列表', type: Array })
|
||||
|
|
@ -202,7 +202,7 @@ export class BatchOperationResultDTOGeneric<T> extends BatchOperationResultDTO {
|
|||
}
|
||||
|
||||
/**
|
||||
* 同步操作结果类(泛型支持)
|
||||
* 同步操作结果类(泛型支持)
|
||||
*/
|
||||
export class SyncOperationResultDTOGeneric<T> extends SyncOperationResultDTO {
|
||||
@ApiProperty({ description: '同步成功的数据列表', type: Array })
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { ApiProperty } from '@midwayjs/swagger';
|
|||
import { UnifiedSearchParamsDTO } from './api.dto';
|
||||
import { Customer } from '../entity/customer.entity';
|
||||
|
||||
// 客户基本信息DTO(用于响应)
|
||||
// 客户基本信息DTO(用于响应)
|
||||
export class CustomerDTO extends Customer{
|
||||
@ApiProperty({ description: '客户ID' })
|
||||
id: number;
|
||||
|
|
@ -163,11 +163,11 @@ export class UpdateCustomerDTO {
|
|||
tags?: string[];
|
||||
}
|
||||
|
||||
// 查询单个客户响应DTO(继承基本信息)
|
||||
// 查询单个客户响应DTO(继承基本信息)
|
||||
export class GetCustomerDTO extends CustomerDTO {
|
||||
// 可以添加额外的详细信息字段
|
||||
}
|
||||
// 客户统计信息DTO(包含订单统计)
|
||||
// 客户统计信息DTO(包含订单统计)
|
||||
export class CustomerStatisticDTO extends CustomerDTO {
|
||||
@ApiProperty({ description: '创建日期' })
|
||||
date_created: Date;
|
||||
|
|
@ -209,7 +209,7 @@ export class CustomerStatisticWhereDTO {
|
|||
customerId?: number;
|
||||
}
|
||||
|
||||
// 客户统计查询参数DTO(继承通用查询参数)
|
||||
// 客户统计查询参数DTO(继承通用查询参数)
|
||||
export type CustomerStatisticQueryParamsDTO = UnifiedSearchParamsDTO<CustomerStatisticWhereDTO>;
|
||||
|
||||
// 客户统计列表响应DTO
|
||||
|
|
@ -259,7 +259,7 @@ export class BatchDeleteCustomerDTO {
|
|||
|
||||
// ====================== 查询操作 ======================
|
||||
|
||||
// 客户查询条件DTO(用于UnifiedSearchParamsDTO的where参数)
|
||||
// 客户查询条件DTO(用于UnifiedSearchParamsDTO的where参数)
|
||||
export class CustomerWhereDTO {
|
||||
@ApiProperty({ description: '邮箱筛选', required: false })
|
||||
email?: string;
|
||||
|
|
@ -284,10 +284,10 @@ export class CustomerWhereDTO {
|
|||
role?: string;
|
||||
}
|
||||
|
||||
// 客户查询参数DTO(继承通用查询参数)
|
||||
// 客户查询参数DTO(继承通用查询参数)
|
||||
export type CustomerQueryParamsDTO = UnifiedSearchParamsDTO<CustomerWhereDTO>;
|
||||
|
||||
// 客户列表响应DTO(参考site-api.dto.ts中的分页格式)
|
||||
// 客户列表响应DTO(参考site-api.dto.ts中的分页格式)
|
||||
export class CustomerListResponseDTO {
|
||||
@ApiProperty({ description: '客户列表', type: [CustomerDTO] })
|
||||
items: CustomerDTO[];
|
||||
|
|
@ -359,6 +359,6 @@ export class SyncCustomersDTO {
|
|||
@ApiProperty({ description: '站点ID' })
|
||||
siteId: number;
|
||||
|
||||
@ApiProperty({ description: '查询参数(支持where和orderBy)', type: UnifiedSearchParamsDTO, required: false })
|
||||
@ApiProperty({ description: '查询参数(支持where和orderBy)', type: UnifiedSearchParamsDTO, required: false })
|
||||
params?: UnifiedSearchParamsDTO<CustomerWhereDTO>;
|
||||
}
|
||||
|
|
@ -22,7 +22,7 @@ export class ShopyyAllProductQuery {
|
|||
id?: string;
|
||||
/** 商品标题,支持模糊查询 */
|
||||
title?: string;
|
||||
/** 商品状态,例如:上架、下架、删除等(具体值参考 Shopyy 接口文档) */
|
||||
/** 商品状态,例如:上架、下架、删除等(具体值参考 Shopyy 接口文档) */
|
||||
status?: string;
|
||||
/** 商品SKU编码,库存保有单位,精确或模糊匹配 */
|
||||
sku?: string;
|
||||
|
|
@ -34,21 +34,21 @@ export class ShopyyAllProductQuery {
|
|||
variant_price_min?: string;
|
||||
/** 变体价格最大值,筛选变体价格小于等于该值的商品 */
|
||||
variant_price_max?: string;
|
||||
/** 变体划线价(原价)最小值,筛选变体划线价大于等于该值的商品 */
|
||||
/** 变体划线价(原价)最小值,筛选变体划线价大于等于该值的商品 */
|
||||
variant_compare_at_price_min?: string;
|
||||
/** 变体划线价(原价)最大值,筛选变体划线价小于等于该值的商品 */
|
||||
/** 变体划线价(原价)最大值,筛选变体划线价小于等于该值的商品 */
|
||||
variant_compare_at_price_max?: string;
|
||||
/** 变体重量最小值,筛选变体重量大于等于该值的商品(单位参考接口文档) */
|
||||
/** 变体重量最小值,筛选变体重量大于等于该值的商品(单位参考接口文档) */
|
||||
variant_weight_min?: string;
|
||||
/** 变体重量最大值,筛选变体重量小于等于该值的商品(单位参考接口文档) */
|
||||
/** 变体重量最大值,筛选变体重量小于等于该值的商品(单位参考接口文档) */
|
||||
variant_weight_max?: string;
|
||||
/** 商品创建时间最小值,格式参考接口文档(如:YYYY-MM-DD HH:mm:ss) */
|
||||
/** 商品创建时间最小值,格式参考接口文档(如:YYYY-MM-DD HH:mm:ss) */
|
||||
created_at_min?: string;
|
||||
/** 商品创建时间最大值,格式参考接口文档(如:YYYY-MM-DD HH:mm:ss) */
|
||||
/** 商品创建时间最大值,格式参考接口文档(如:YYYY-MM-DD HH:mm:ss) */
|
||||
created_at_max?: string;
|
||||
/** 商品更新时间最小值,格式参考接口文档(如:YYYY-MM-DD HH:mm:ss) */
|
||||
/** 商品更新时间最小值,格式参考接口文档(如:YYYY-MM-DD HH:mm:ss) */
|
||||
updated_at_min?: string;
|
||||
/** 商品更新时间最大值,格式参考接口文档(如:YYYY-MM-DD HH:mm:ss) */
|
||||
/** 商品更新时间最大值,格式参考接口文档(如:YYYY-MM-DD HH:mm:ss) */
|
||||
updated_at_max?: string;
|
||||
}
|
||||
// 产品类型
|
||||
|
|
@ -133,9 +133,9 @@ export interface ShopyyVariant {
|
|||
//
|
||||
// 订单查询参数类型
|
||||
export interface ShopyyOrderQuery {
|
||||
// 订单ID集合 多个ID用','联接 例:1,2,3
|
||||
// 订单ID集合 多个ID用','联接 例:1,2,3
|
||||
ids?: string;
|
||||
// 订单状态 100 未完成;110 待处理;180 已完成(确认收货); 190 取消;
|
||||
// 订单状态 100 未完成;110 待处理;180 已完成(确认收货); 190 取消;
|
||||
status?: string;
|
||||
// 物流状态 300 未发货;310 部分发货;320 已发货;330(确认收货)
|
||||
fulfillment_status?: string;
|
||||
|
|
@ -159,9 +159,9 @@ export interface ShopyyOrderQuery {
|
|||
page?: string;
|
||||
// 每页条数
|
||||
limit?: string;
|
||||
// 排序字段(默认id) id=订单ID updated_at=最后更新时间 pay_at=支付时间
|
||||
// 排序字段(默认id) id=订单ID updated_at=最后更新时间 pay_at=支付时间
|
||||
order_field?: string;
|
||||
// 排序方式(默认desc) desc=降序 asc=升序
|
||||
// 排序方式(默认desc) desc=降序 asc=升序
|
||||
order_by?: string;
|
||||
// 订单列表类型
|
||||
group?: string;
|
||||
|
|
@ -513,7 +513,7 @@ export class ShopyyFulfillmentDTO {
|
|||
"tracking_number": string;
|
||||
"courier_code": number;
|
||||
"note": string;
|
||||
"mode": "replace" | 'cover' | null// 模式 replace(替换) cover (覆盖) 空(新增)
|
||||
"mode": "replace" | 'cover' | null// 模式 replace(替换) cover (覆盖) 空(新增)
|
||||
}
|
||||
// https://www.apizza.net/project/e114fb8e628e0f604379f5b26f0d8330/browse
|
||||
export class ShopyPartFulfillmentDTO {
|
||||
|
|
|
|||
|
|
@ -141,7 +141,7 @@ export class UnifiedProductAttributeDTO {
|
|||
@ApiProperty({ description: '属性选项', type: [String] })
|
||||
options: string[];
|
||||
|
||||
@ApiProperty({ description: '变体属性值(单个值)', required: false })
|
||||
@ApiProperty({ description: '变体属性值(单个值)', required: false })
|
||||
option?: string;
|
||||
// 这个是属性的父级字典项
|
||||
dict?: Dict;
|
||||
|
|
|
|||
|
|
@ -152,7 +152,7 @@ export class QuerySiteDTO {
|
|||
@Rule(RuleType.boolean().optional())
|
||||
isDisabled?: boolean;
|
||||
|
||||
@ApiProperty({ description: '站点ID列表(逗号分隔)', required: false })
|
||||
@ApiProperty({ description: '站点ID列表(逗号分隔)', required: false })
|
||||
@Rule(RuleType.string().optional())
|
||||
ids?: string;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,11 +8,11 @@ export interface WooProduct {
|
|||
id: number;
|
||||
// 创建时间
|
||||
date_created: string;
|
||||
// 创建时间(GMT)
|
||||
// 创建时间(GMT)
|
||||
date_created_gmt: string;
|
||||
// 更新时间
|
||||
date_modified: string;
|
||||
// 更新时间(GMT)
|
||||
// 更新时间(GMT)
|
||||
date_modified_gmt: string;
|
||||
// 产品类型 simple grouped external variable
|
||||
type: string;
|
||||
|
|
@ -20,7 +20,7 @@ export interface WooProduct {
|
|||
status: string;
|
||||
// 是否为特色产品
|
||||
featured: boolean;
|
||||
// 目录可见性选项:visible, catalog, search and hidden. Default is visible.
|
||||
// 目录可见性选项:visible, catalog, search and hidden. Default is visible.
|
||||
catalog_visibility: string;
|
||||
|
||||
// 常规价格
|
||||
|
|
@ -130,11 +130,11 @@ export interface WooVariation {
|
|||
id: number;
|
||||
// 创建时间
|
||||
date_created: string;
|
||||
// 创建时间(GMT)
|
||||
// 创建时间(GMT)
|
||||
date_created_gmt: string;
|
||||
// 更新时间
|
||||
date_modified: string;
|
||||
// 更新时间(GMT)
|
||||
// 更新时间(GMT)
|
||||
date_modified_gmt: string;
|
||||
// 变体描述
|
||||
description: string;
|
||||
|
|
@ -150,11 +150,11 @@ export interface WooVariation {
|
|||
price_html?: string;
|
||||
// 促销开始日期
|
||||
date_on_sale_from?: string;
|
||||
// 促销开始日期(GMT)
|
||||
// 促销开始日期(GMT)
|
||||
date_on_sale_from_gmt?: string;
|
||||
// 促销结束日期
|
||||
date_on_sale_to?: string;
|
||||
// 促销结束日期(GMT)
|
||||
// 促销结束日期(GMT)
|
||||
date_on_sale_to_gmt?: string;
|
||||
// 是否在促销中
|
||||
on_sale: boolean;
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ export enum OrderStatus {
|
|||
REFUNDED = 'refunded', // 已退款
|
||||
FAILED = 'failed', // 失败订单
|
||||
DRAFT = 'draft', // 草稿
|
||||
AUTO_DRAFT = 'auto-draft', // 自动草稿(TODO:不知道为什么出现)
|
||||
AUTO_DRAFT = 'auto-draft', // 自动草稿(TODO:不知道为什么出现)
|
||||
|
||||
// TRASH = 'trash',
|
||||
// refund 也就是退款相关的状态
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@ export interface IPlatformService {
|
|||
getOrder(siteId: number, orderId: string): Promise<any>;
|
||||
|
||||
/**
|
||||
* 获取订阅列表(如果平台支持)
|
||||
* 获取订阅列表(如果平台支持)
|
||||
* @param siteId 站点ID
|
||||
* @returns 订阅列表数据
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -36,13 +36,11 @@ export interface ISiteAdapter {
|
|||
mapUnifiedToPlatformCustomer(data: Partial<UnifiedCustomerDTO>): any;
|
||||
|
||||
/**
|
||||
* 获取单个客户
|
||||
* 获取单个客户
|
||||
*/
|
||||
getCustomer(where: Partial<Pick<UnifiedCustomerDTO, 'id' | 'email' | 'phone'>>): Promise<UnifiedCustomerDTO>;
|
||||
|
||||
/**
|
||||
* 获取客户列表
|
||||
* 获取客户列表
|
||||
*/
|
||||
getCustomers(params: UnifiedSearchParamsDTO): Promise<UnifiedPaginationDTO<UnifiedCustomerDTO>>;
|
||||
|
|
@ -70,7 +68,6 @@ export interface ISiteAdapter {
|
|||
/**
|
||||
* 批量处理客户
|
||||
*/
|
||||
|
||||
batchProcessCustomers?(data: BatchOperationDTO): Promise<BatchOperationResultDTO>;
|
||||
|
||||
// ========== 媒体映射方法 ==========
|
||||
|
|
|
|||
|
|
@ -66,7 +66,7 @@ export class CustomerService {
|
|||
}
|
||||
|
||||
if (typeof dateValue === 'number') {
|
||||
// 处理Unix时间戳(秒或毫秒)
|
||||
// 处理Unix时间戳(秒或毫秒)
|
||||
return new Date(dateValue > 9999999999 ? dateValue : dateValue * 1000);
|
||||
}
|
||||
|
||||
|
|
@ -95,7 +95,7 @@ export class CustomerService {
|
|||
}
|
||||
|
||||
/**
|
||||
* 创建或更新客户(upsert)
|
||||
* 创建或更新客户(upsert)
|
||||
* 如果客户存在则更新,不存在则创建
|
||||
*/
|
||||
async upsertCustomer(
|
||||
|
|
@ -157,24 +157,24 @@ export class CustomerService {
|
|||
|
||||
/**
|
||||
* 从站点同步客户数据
|
||||
* 第一步:调用adapter获取站点客户数据
|
||||
* 第二步:通过upsertManyCustomers保存这些客户
|
||||
* 第一步:调用adapter获取站点客户数据
|
||||
* 第二步:通过upsertManyCustomers保存这些客户
|
||||
*/
|
||||
async syncCustomersFromSite(
|
||||
siteId: number,
|
||||
params?: UnifiedSearchParamsDTO
|
||||
): Promise<SyncOperationResult> {
|
||||
try {
|
||||
// 第一步:获取适配器并从站点获取客户数据
|
||||
// 第一步:获取适配器并从站点获取客户数据
|
||||
const adapter = await this.siteApiService.getAdapter(siteId);
|
||||
const siteCustomers = await adapter.getAllCustomers(params || {});
|
||||
|
||||
// 第二步:将站点客户数据转换为客户实体数据
|
||||
// 第二步:将站点客户数据转换为客户实体数据
|
||||
const customersData = siteCustomers.map(siteCustomer => {
|
||||
return this.mapSiteCustomerToCustomer(siteCustomer, siteId);
|
||||
})
|
||||
|
||||
// 第三步:批量upsert客户数据
|
||||
// 第三步:批量upsert客户数据
|
||||
const upsertResult = await this.upsertManyCustomers(customersData);
|
||||
return {
|
||||
total: siteCustomers.length,
|
||||
|
|
@ -192,7 +192,7 @@ export class CustomerService {
|
|||
}
|
||||
|
||||
/**
|
||||
* 获取客户统计列表(包含订单统计信息)
|
||||
* 获取客户统计列表(包含订单统计信息)
|
||||
* 支持分页、搜索和排序功能
|
||||
* 使用原生SQL查询实现复杂的统计逻辑
|
||||
*/
|
||||
|
|
@ -363,7 +363,7 @@ export class CustomerService {
|
|||
}
|
||||
|
||||
/**
|
||||
* 获取纯粹的客户列表(不包含订单统计信息)
|
||||
* 获取纯粹的客户列表(不包含订单统计信息)
|
||||
* 支持基本的分页、搜索和排序功能
|
||||
* 使用TypeORM查询构建器实现
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -239,7 +239,7 @@ export class DictService {
|
|||
}
|
||||
|
||||
// 更新或创建字典项 (Upsert)
|
||||
// 如果字典项已存在(根据 name 和 dictId 判断),则更新;否则创建新的
|
||||
// 如果字典项已存在(根据 name 和 dictId 判断),则更新;否则创建新的
|
||||
async upsertDictItem(dictId: number, itemData: {
|
||||
name: string;
|
||||
title: string;
|
||||
|
|
@ -252,7 +252,7 @@ export class DictService {
|
|||
// 格式化 name
|
||||
const formattedName = this.formatName(itemData.name);
|
||||
|
||||
// 查找是否已存在该字典项(根据 name 和 dictId)
|
||||
// 查找是否已存在该字典项(根据 name 和 dictId)
|
||||
const existingItem = await this.dictItemModel.findOne({
|
||||
where: {
|
||||
name: formattedName,
|
||||
|
|
|
|||
|
|
@ -479,7 +479,7 @@ export class OrderService {
|
|||
// 如果不能更新 ERP 状态,则保留原有的 orderStatus
|
||||
entity.orderStatus = existingOrder.orderStatus;
|
||||
}
|
||||
// 更新订单数据(包括 shipping、billing 等字段)
|
||||
// 更新订单数据(包括 shipping、billing 等字段)
|
||||
await this.orderModel.update(existingOrder.id, entity);
|
||||
entity.id = existingOrder.id;
|
||||
return entity;
|
||||
|
|
@ -2567,8 +2567,8 @@ export class OrderService {
|
|||
* 导出数据为CSV格式
|
||||
* @param {any[]} data 数据数组
|
||||
* @param {Object} options 配置选项
|
||||
* @param {string} [options.type='string'] 输出类型:'string' | 'buffer'
|
||||
* @param {string} [options.fileName] 文件名(仅当需要写入文件时使用)
|
||||
* @param {string} [options.type='string'] 输出类型:'string' | 'buffer'
|
||||
* @param {string} [options.fileName] 文件名(仅当需要写入文件时使用)
|
||||
* @param {boolean} [options.writeFile=false] 是否写入文件
|
||||
* @returns {string|Buffer} 根据type返回字符串或Buffer
|
||||
*/
|
||||
|
|
@ -2617,7 +2617,7 @@ async exportToCsv(data: any[], options: { type?: 'string' | 'buffer'; fileName?:
|
|||
// 获取当前用户目录
|
||||
const userHomeDir = os.homedir();
|
||||
|
||||
// 构建目标路径(下载目录)
|
||||
// 构建目标路径(下载目录)
|
||||
const downloadsDir = path.join(userHomeDir, 'Downloads');
|
||||
|
||||
// 确保下载目录存在
|
||||
|
|
|
|||
|
|
@ -235,7 +235,7 @@ export class ProductService {
|
|||
.leftJoinAndSelect('product.attributes', 'attribute')
|
||||
.leftJoinAndSelect('attribute.dict', 'dict')
|
||||
.leftJoinAndSelect('product.category', 'category');
|
||||
// 处理分页参数(支持新旧两种格式)
|
||||
// 处理分页参数(支持新旧两种格式)
|
||||
const page = query.page || 1;
|
||||
const pageSize = query.per_page || 10;
|
||||
|
||||
|
|
@ -393,7 +393,7 @@ export class ProductService {
|
|||
qb.andWhere('product.updatedAt <= :whereUpdatedAtEnd', { whereUpdatedAtEnd: new Date(query.where.updatedAtEnd) });
|
||||
}
|
||||
|
||||
// 品牌过滤(向后兼容)
|
||||
// 品牌过滤(向后兼容)
|
||||
if (brandId) {
|
||||
qb.andWhere(qb => {
|
||||
const subQuery = qb
|
||||
|
|
@ -423,7 +423,7 @@ export class ProductService {
|
|||
});
|
||||
}
|
||||
|
||||
// 分类过滤(向后兼容)
|
||||
// 分类过滤(向后兼容)
|
||||
if (categoryId) {
|
||||
qb.andWhere('product.categoryId = :categoryId', { categoryId });
|
||||
}
|
||||
|
|
@ -443,7 +443,7 @@ export class ProductService {
|
|||
qb.andWhere('product.categoryId IN (:...whereCategoryIds)', { whereCategoryIds: query.where.categoryIds });
|
||||
}
|
||||
|
||||
// 处理排序(支持新旧两种格式)
|
||||
// 处理排序(支持新旧两种格式)
|
||||
if (orderBy) {
|
||||
if (typeof orderBy === 'string') {
|
||||
// 如果orderBy是字符串,尝试解析JSON
|
||||
|
|
@ -1765,7 +1765,7 @@ export class ProductService {
|
|||
}
|
||||
|
||||
|
||||
// 根据ID获取产品详情(包含站点SKU)
|
||||
// 根据ID获取产品详情(包含站点SKU)
|
||||
async getProductById(id: number): Promise<Product> {
|
||||
const product = await this.productModel.findOne({
|
||||
where: { id },
|
||||
|
|
|
|||
|
|
@ -128,7 +128,7 @@ export class ShopyyService {
|
|||
* @returns 完整URL
|
||||
*/
|
||||
private buildURL(baseUrl: string, endpoint: string): string {
|
||||
// ShopYY API URL格式:https://{shop}.shopyy.com/openapi/{version}/{endpoint}
|
||||
// ShopYY API URL格式:https://{shop}.shopyy.com/openapi/{version}/{endpoint}
|
||||
const base = baseUrl.replace(/\/$/, '');
|
||||
const end = endpoint.replace(/^\//, '');
|
||||
return `${base}/${end}`;
|
||||
|
|
|
|||
|
|
@ -152,7 +152,7 @@ export class SiteApiService {
|
|||
const result = await this.upsertProduct(siteId, product);
|
||||
// 判断是创建还是更新
|
||||
if (result && result.id) {
|
||||
// 简单判断:如果产品原本没有ID而现在有了,说明是创建的
|
||||
// 简单判断:如果产品原本没有ID而现在有了,说明是创建的
|
||||
if (!product.id || !product.id.toString().trim()) {
|
||||
results.created.push(result);
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -254,7 +254,7 @@ export class WPService implements IPlatformService {
|
|||
}
|
||||
|
||||
|
||||
// 导出 WooCommerce 产品为特殊CSV(平台特性)
|
||||
// 导出 WooCommerce 产品为特殊CSV(平台特性)
|
||||
async exportProductsCsvSpecial(site: any, page: number = 1, pageSize: number = 100): Promise<string> {
|
||||
const list = await this.getProducts(site, { page, per_page: pageSize });
|
||||
const header = ['id','name','type','status','sku','regular_price','sale_price','stock_status','stock_quantity'];
|
||||
|
|
|
|||
|
|
@ -0,0 +1,105 @@
|
|||
# 产品站点SKU查询方法测试文档
|
||||
|
||||
## 新增和更新的API接口
|
||||
|
||||
### 1. 根据产品ID获取站点SKU列表
|
||||
**接口**: `GET /product/:id/site-skus`
|
||||
**功能**: 获取指定产品的所有站点SKU列表
|
||||
**返回**: 站点SKU对象数组,按创建时间升序排列
|
||||
|
||||
### 2. 根据站点SKU查询产品
|
||||
**接口**: `GET /product/site-sku/:siteSku`
|
||||
**功能**: 根据站点SKU代码查询对应的产品信息
|
||||
**返回**: 完整的产品对象(包含站点SKU、分类、属性等关联数据)
|
||||
|
||||
### 3. 根据产品ID获取产品详情
|
||||
**接口**: `GET /product/:id`
|
||||
**功能**: 获取产品的完整详情信息
|
||||
**返回**: 完整的产品对象(包含站点SKU、分类、属性、组成等关联数据)
|
||||
|
||||
### 4. 现有接口的增强
|
||||
|
||||
#### 4.1 根据SKU查询产品
|
||||
**接口**: `GET /product/sku/:sku`
|
||||
**增强**: 现在返回的产品信息包含关联的站点SKU数据
|
||||
|
||||
#### 4.2 搜索产品
|
||||
**接口**: `GET /product/search?name=:name`
|
||||
**增强**: 搜索结果现在包含每个产品的站点SKU数据
|
||||
|
||||
#### 4.3 获取产品列表
|
||||
**接口**: `GET /product/list`
|
||||
**增强**: 产品列表中的每个产品现在都包含站点SKU数据
|
||||
|
||||
## 服务层新增方法
|
||||
|
||||
### ProductService新增方法
|
||||
|
||||
1. **getProductSiteSkus(productId: number)**: Promise<ProductSiteSku[]>
|
||||
- 获取指定产品的所有站点SKU
|
||||
- 包含产品关联信息
|
||||
- 按创建时间排序
|
||||
|
||||
2. **getProductById(id: number)**: Promise<Product>
|
||||
- 根据产品ID获取完整产品信息
|
||||
- 包含站点SKU、分类、属性、组成等所有关联数据
|
||||
- 自动处理单品和混装商品的组成信息
|
||||
|
||||
3. **findProductBySiteSku(siteSku: string)**: Promise<Product>
|
||||
- 根据站点SKU查询对应的产品
|
||||
- 返回完整的产品信息
|
||||
- 如果站点SKU不存在则抛出错误
|
||||
|
||||
### 现有方法增强
|
||||
|
||||
1. **findProductsByName(name: string)**: 现在包含站点SKU数据
|
||||
2. **findProductBySku(sku: string)**: 现在包含站点SKU数据
|
||||
3. **getProductList**: 已经包含站点SKU数据(无需更改)
|
||||
|
||||
## 使用示例
|
||||
|
||||
### 获取产品的站点SKU列表
|
||||
```javascript
|
||||
// GET /product/123/site-skus
|
||||
// 返回:
|
||||
[
|
||||
{
|
||||
"id": 1,
|
||||
"siteSku": "SITE-SKU-001",
|
||||
"productId": 123,
|
||||
"createdAt": "2024-01-01T00:00:00Z",
|
||||
"updatedAt": "2024-01-01T00:00:00Z"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
### 根据站点SKU查询产品
|
||||
```javascript
|
||||
// GET /product/site-sku/SITE-SKU-001
|
||||
// 返回完整的产品对象,包含:
|
||||
// - 基本信息(SKU、名称、价格等)
|
||||
// - 分类信息
|
||||
// - 属性信息
|
||||
// - 站点SKU列表
|
||||
// - 组成信息
|
||||
```
|
||||
|
||||
### 获取产品详情
|
||||
```javascript
|
||||
// GET /product/123
|
||||
// 返回完整的产品对象,与站点SKU查询类似
|
||||
```
|
||||
|
||||
## 数据库查询优化
|
||||
|
||||
所有新增和更新的方法都使用了TypeORM的关联查询,确保:
|
||||
- 一次查询获取所有需要的数据
|
||||
- 避免N+1查询问题
|
||||
- 包含必要的关联关系(分类、属性、站点SKU、组成)
|
||||
|
||||
## 错误处理
|
||||
|
||||
所有方法都包含适当的错误处理:
|
||||
- 产品不存在时抛出明确的错误信息
|
||||
- 站点SKU不存在时抛出明确的错误信息
|
||||
- 控制器层统一处理错误并返回适当的HTTP响应
|
||||
Loading…
Reference in New Issue