feat(订单): 添加获取订单总数功能

实现订单总数统计接口,包括:
1. 在ISiteAdapter接口添加countOrders方法
2. 在WooCommerce和Shopyy适配器中实现该方法
3. 添加控制器端点暴露该功能
4. 优化订单查询参数映射逻辑

refactor(Shopyy): 重构搜索参数映射逻辑

将通用的搜索参数映射逻辑提取为独立方法,提高代码复用性

refactor(interface): 重构站点适配器接口,按功能模块组织方法

重构 ISiteAdapter 接口,将相关方法按功能模块(客户、媒体、订单、产品等)分组
移除废弃的 fulfillOrder 方法
新增多个数据映射方法以支持统一数据格式转换

refactor(api): 统一接口参数为对象形式并支持多条件查询

重构所有接口方法,将直接传递id参数改为接受where条件对象
支持通过id、sku、email等多条件查询实体
优化产品服务逻辑,支持通过sku直接查询产品
统一各适配器实现,确保接口一致性

feat(订单): 添加获取订单总数功能

实现订单总数统计接口,包括:
1. 在ISiteAdapter接口添加countOrders方法
2. 在WooCommerce和Shopyy适配器中实现该方法
3. 添加控制器端点暴露该功能
4. 优化订单查询参数映射逻辑

refactor(Shopyy): 重构搜索参数映射逻辑

将通用的搜索参数映射逻辑提取为独立方法,提高代码复用性

refactor(interface): 重构站点适配器接口,按功能模块组织方法

重构 ISiteAdapter 接口,将相关方法按功能模块(客户、媒体、订单、产品等)分组
移除废弃的 fulfillOrder 方法
新增多个数据映射方法以支持统一数据格式转换

refactor(api): 统一接口参数为对象形式并支持多条件查询

重构所有接口方法,将直接传递id参数改为接受where条件对象
支持通过id、sku、email等多条件查询实体
优化产品服务逻辑,支持通过sku直接查询产品
统一各适配器实现,确保接口一致性

feat: 增强产品同步功能并优化SKU生成逻辑

添加字典排序字段支持
优化产品同步流程,支持通过SKU同步
重构SKU模板生成逻辑,支持分类属性排序
完善产品导入导出功能,增加分类字段处理
统一产品操作方法,提升代码可维护性

fix(sync_shipment): 捕获运单状态更新时的异常并记录日志

添加try-catch块来捕获updateShipmentState过程中可能出现的错误
使用logger记录错误信息以便后续排查

feat(shopyy): 实现全量商品查询功能并优化产品相关逻辑

- 新增ShopyyAllProductQuery类支持全量商品查询参数
- 实现getAllProducts方法支持带条件查询
- 优化getProductBySku方法使用新查询接口
- 公开request方法便于子类调用
- 增加错误日志记录产品查找失败情况
- 修复产品permalink生成逻辑

refactor(adapter): 清理重复代码并统一订单映射方法命名

移除shopyy和woocommerce适配器中的重复代码
将mapOrder重命名为mapPlatformToUnifiedOrder以保持命名一致性

feat(订单): 添加获取订单总数功能

实现订单总数统计接口,包括:
1. 在ISiteAdapter接口添加countOrders方法
2. 在WooCommerce和Shopyy适配器中实现该方法
3. 添加控制器端点暴露该功能
4. 优化订单查询参数映射逻辑

refactor(Shopyy): 重构搜索参数映射逻辑

将通用的搜索参数映射逻辑提取为独立方法,提高代码复用性

refactor(interface): 重构站点适配器接口,按功能模块组织方法

重构 ISiteAdapter 接口,将相关方法按功能模块(客户、媒体、订单、产品等)分组
移除废弃的 fulfillOrder 方法
新增多个数据映射方法以支持统一数据格式转换

refactor(api): 统一接口参数为对象形式并支持多条件查询

重构所有接口方法,将直接传递id参数改为接受where条件对象
支持通过id、sku、email等多条件查询实体
优化产品服务逻辑,支持通过sku直接查询产品
统一各适配器实现,确保接口一致性

feat(订单): 添加获取订单总数功能

实现订单总数统计接口,包括:
1. 在ISiteAdapter接口添加countOrders方法
2. 在WooCommerce和Shopyy适配器中实现该方法
3. 添加控制器端点暴露该功能
4. 优化订单查询参数映射逻辑

refactor(Shopyy): 重构搜索参数映射逻辑

将通用的搜索参数映射逻辑提取为独立方法,提高代码复用性

refactor(interface): 重构站点适配器接口,按功能模块组织方法

重构 ISiteAdapter 接口,将相关方法按功能模块(客户、媒体、订单、产品等)分组
移除废弃的 fulfillOrder 方法
新增多个数据映射方法以支持统一数据格式转换

Test

refactor(api): 统一接口参数为对象形式并支持多条件查询

重构所有接口方法,将直接传递id参数改为接受where条件对象
支持通过id、sku、email等多条件查询实体
优化产品服务逻辑,支持通过sku直接查询产品
统一各适配器实现,确保接口一致性

feat(shopyy): 实现全量商品查询功能并优化产品相关逻辑

- 新增ShopyyAllProductQuery类支持全量商品查询参数
- 实现getAllProducts方法支持带条件查询
- 优化getProductBySku方法使用新查询接口
- 公开request方法便于子类调用
- 增加错误日志记录产品查找失败情况
- 修复产品permalink生成逻辑

docs: 统一中文括号格式为全角括号

将代码中的中文括号格式从半角"()"统一修改为全角"()",并删除测试文档文件test-site-sku-methods.md

chore: config.local 还原

docs(dto): 修正注释中的中文括号格式

docs(dto): 修正注释中的括号格式

docs: 修正中文标点符号和注释格式

统一将中文注释和文档中的全角括号和冒号改为半角格式
修正部分TODO注释的标点符号
统一接口文档中的描述符号格式
This commit is contained in:
tikkhun 2026-01-07 15:22:18 +08:00
parent 983ba47dbf
commit 0265a642d8
37 changed files with 2641 additions and 2061 deletions

17
package-lock.json generated
View File

@ -523,23 +523,6 @@
"node": ">=18" "node": ">=18"
} }
}, },
"node_modules/@faker-js/faker": {
"version": "10.1.0",
"resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-10.1.0.tgz",
"integrity": "sha512-C3mrr3b5dRVlKPJdfrAXS8+dq+rq8Qm5SNRazca0JKgw1HQERFmrVb0towvMmw5uu8hHKNiQasMaR/tydf3Zsg==",
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/fakerjs"
}
],
"license": "MIT",
"peer": true,
"engines": {
"node": "^20.19.0 || ^22.13.0 || ^23.5.0 || >=24.0.0",
"npm": ">=10"
}
},
"node_modules/@hapi/bourne": { "node_modules/@hapi/bourne": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmmirror.com/@hapi/bourne/-/bourne-3.0.0.tgz", "resolved": "https://registry.npmmirror.com/@hapi/bourne/-/bourne-3.0.0.tgz",

View File

@ -1,184 +0,0 @@
# 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. 验证现有产品匹配是否正确

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -7,8 +7,11 @@ export default {
// dataSource: { // dataSource: {
// default: { // default: {
// host: '13.212.62.127', // host: '13.212.62.127',
// port: "3306",
// username: 'root', // username: 'root',
// password: 'Yoone!@.2025', // password: 'Yoone!@.2025',
// database: 'inventory_v2',
// synchronize: true,
// }, // },
// }, // },
// }, // },
@ -19,7 +22,7 @@ export default {
port: "3306", port: "3306",
username: 'root', username: 'root',
password: 'root', password: 'root',
database: 'inventory', database: 'inventory'
}, },
}, },
}, },

View File

@ -99,7 +99,7 @@ export class MainConfiguration {
} }
/** /**
* * ()
*/ */
private async initializeDatabase(): Promise<void> { private async initializeDatabase(): Promise<void> {
// 使用注入的数据库配置 // 使用注入的数据库配置

View File

@ -1,79 +0,0 @@
import { Controller, Get, Inject, Query, Post, Del, Param, Files, Fields, Body } from '@midwayjs/core';
import { WPService } from '../service/wp.service';
import { successResponse, errorResponse } from '../utils/response.util';
@Controller('/media')
export class MediaController {
@Inject()
wpService: WPService;
@Get('/list')
async list(
@Query('siteId') siteId: number,
@Query('page') page: number = 1,
@Query('pageSize') pageSize: number = 20
) {
try {
if (!siteId) {
return errorResponse('siteId is required');
}
const result = await this.wpService.getMedia(siteId, page, pageSize);
return successResponse(result);
} catch (error) {
return errorResponse(error.message);
}
}
@Post('/upload')
async upload(@Fields() fields, @Files() files) {
try {
const siteId = fields.siteId;
if (!siteId) {
return errorResponse('siteId is required');
}
if (!files || files.length === 0) {
return errorResponse('file is required');
}
const file = files[0];
const result = await this.wpService.createMedia(siteId, file);
return successResponse(result);
} catch (error) {
return errorResponse(error.message);
}
}
@Post('/update/:id')
async update(@Param('id') id: number, @Body() body) {
try {
const siteId = body.siteId;
if (!siteId) {
return errorResponse('siteId is required');
}
// 过滤出需要更新的字段
const { title, caption, description, alt_text } = body;
const data: any = {};
if (title !== undefined) data.title = title;
if (caption !== undefined) data.caption = caption;
if (description !== undefined) data.description = description;
if (alt_text !== undefined) data.alt_text = alt_text;
const result = await this.wpService.updateMedia(siteId, id, data);
return successResponse(result);
} catch (error) {
return errorResponse(error.message);
}
}
@Del('/:id')
async delete(@Param('id') id: number, @Query('siteId') siteId: number, @Query('force') force: boolean = true) {
try {
if (!siteId) {
return errorResponse('siteId is required');
}
const result = await this.wpService.deleteMedia(siteId, id, force);
return successResponse(result);
} catch (error) {
return errorResponse(error.message);
}
}
}

View File

@ -698,10 +698,10 @@ export class ProductController {
// 从站点同步产品到本地 // 从站点同步产品到本地
@ApiOkResponse({ description: '从站点同步产品到本地', type: ProductRes }) @ApiOkResponse({ description: '从站点同步产品到本地', type: ProductRes })
@Post('/sync-from-site') @Post('/sync-from-site')
async syncProductFromSite(@Body() body: { siteId: number; siteProductId: string | number }) { async syncProductFromSite(@Body() body: { siteId: number; siteProductId: string | number ,sku: string}) {
try { try {
const { siteId, siteProductId } = body; const { siteId, siteProductId, sku } = body;
const product = await this.productService.syncProductFromSite(siteId, siteProductId); const product = await this.productService.syncProductFromSite(siteId, siteProductId, sku);
return successResponse(product); return successResponse(product);
} catch (error) { } catch (error) {
return errorResponse(error?.message || error); return errorResponse(error?.message || error);
@ -713,25 +713,26 @@ export class ProductController {
@Post('/batch-sync-from-site') @Post('/batch-sync-from-site')
async batchSyncFromSite(@Body() body: { siteId: number; siteProductIds: (string | number)[] }) { async batchSyncFromSite(@Body() body: { siteId: number; siteProductIds: (string | number)[] }) {
try { try {
const { siteId, siteProductIds } = body; throw new Error('批量同步产品到本地暂未实现');
const result = await this.productService.batchSyncFromSite(siteId, siteProductIds); // const { siteId, siteProductIds } = body;
// 将服务层返回的结果转换为统一格式 // const result = await this.productService.batchSyncFromSite(siteId, siteProductIds.map((id) => ({ siteProductId: id, sku: '' })));
const errors = result.errors.map((error: string) => { // // 将服务层返回的结果转换为统一格式
// 提取产品ID部分作为标识符 // const errors = result.errors.map((error: string) => {
const match = error.match(/站点产品ID (\d+) /); // // 提取产品ID部分作为标识符
const identifier = match ? match[1] : 'unknown'; // const match = error.match(/站点产品ID (\d+) /);
return { // const identifier = match ? match[1] : 'unknown';
identifier: identifier, // return {
error: error // identifier: identifier,
}; // error: error
}); // };
// });
return successResponse({ // return successResponse({
total: siteProductIds.length, // total: siteProductIds.length,
processed: result.synced + errors.length, // processed: result.synced + errors.length,
synced: result.synced, // synced: result.synced,
errors: errors // errors: errors
}); // });
} catch (error) { } catch (error) {
return errorResponse(error?.message || error); return errorResponse(error?.message || error);
} }

View File

@ -7,7 +7,6 @@ import {
CancelFulfillmentDTO, CancelFulfillmentDTO,
CreateReviewDTO, CreateReviewDTO,
CreateWebhookDTO, CreateWebhookDTO,
FulfillmentDTO,
UnifiedCustomerDTO, UnifiedCustomerDTO,
UnifiedCustomerPaginationDTO, UnifiedCustomerPaginationDTO,
UnifiedMediaPaginationDTO, UnifiedMediaPaginationDTO,
@ -106,7 +105,7 @@ export class SiteApiController {
this.logger.debug(`[Site API] 更新评论开始, siteId: ${siteId}, id: ${id}, body: ${JSON.stringify(body)}`); this.logger.debug(`[Site API] 更新评论开始, siteId: ${siteId}, id: ${id}, body: ${JSON.stringify(body)}`);
try { try {
const adapter = await this.siteApiService.getAdapter(siteId); const adapter = await this.siteApiService.getAdapter(siteId);
const data = await adapter.updateReview(id, body); const data = await adapter.updateReview({ id }, body);
this.logger.debug(`[Site API] 更新评论成功, siteId: ${siteId}, id: ${id}`); this.logger.debug(`[Site API] 更新评论成功, siteId: ${siteId}, id: ${id}`);
return successResponse(data); return successResponse(data);
} catch (error) { } catch (error) {
@ -124,7 +123,7 @@ export class SiteApiController {
this.logger.debug(`[Site API] 删除评论开始, siteId: ${siteId}, id: ${id}`); this.logger.debug(`[Site API] 删除评论开始, siteId: ${siteId}, id: ${id}`);
try { try {
const adapter = await this.siteApiService.getAdapter(siteId); const adapter = await this.siteApiService.getAdapter(siteId);
const data = await adapter.deleteReview(id); const data = await adapter.deleteReview({ id });
this.logger.debug(`[Site API] 删除评论成功, siteId: ${siteId}, id: ${id}`); this.logger.debug(`[Site API] 删除评论成功, siteId: ${siteId}, id: ${id}`);
return successResponse(data); return successResponse(data);
} catch (error) { } catch (error) {
@ -160,7 +159,7 @@ export class SiteApiController {
this.logger.debug(`[Site API] 获取单个webhook开始, siteId: ${siteId}, id: ${id}`); this.logger.debug(`[Site API] 获取单个webhook开始, siteId: ${siteId}, id: ${id}`);
try { try {
const adapter = await this.siteApiService.getAdapter(siteId); const adapter = await this.siteApiService.getAdapter(siteId);
const data = await adapter.getWebhook(id); const data = await adapter.getWebhook({ id });
this.logger.debug(`[Site API] 获取单个webhook成功, siteId: ${siteId}, id: ${id}`); this.logger.debug(`[Site API] 获取单个webhook成功, siteId: ${siteId}, id: ${id}`);
return successResponse(data); return successResponse(data);
} catch (error) { } catch (error) {
@ -199,7 +198,7 @@ export class SiteApiController {
this.logger.debug(`[Site API] 更新webhook开始, siteId: ${siteId}, id: ${id}, body: ${JSON.stringify(body)}`); this.logger.debug(`[Site API] 更新webhook开始, siteId: ${siteId}, id: ${id}, body: ${JSON.stringify(body)}`);
try { try {
const adapter = await this.siteApiService.getAdapter(siteId); const adapter = await this.siteApiService.getAdapter(siteId);
const data = await adapter.updateWebhook(id, body); const data = await adapter.updateWebhook({ id }, body);
this.logger.debug(`[Site API] 更新webhook成功, siteId: ${siteId}, id: ${id}`); this.logger.debug(`[Site API] 更新webhook成功, siteId: ${siteId}, id: ${id}`);
return successResponse(data); return successResponse(data);
} catch (error) { } catch (error) {
@ -217,7 +216,7 @@ export class SiteApiController {
this.logger.debug(`[Site API] 删除webhook开始, siteId: ${siteId}, id: ${id}`); this.logger.debug(`[Site API] 删除webhook开始, siteId: ${siteId}, id: ${id}`);
try { try {
const adapter = await this.siteApiService.getAdapter(siteId); const adapter = await this.siteApiService.getAdapter(siteId);
const data = await adapter.deleteWebhook(id); const data = await adapter.deleteWebhook({ id });
this.logger.debug(`[Site API] 删除webhook成功, siteId: ${siteId}, id: ${id}`); this.logger.debug(`[Site API] 删除webhook成功, siteId: ${siteId}, id: ${id}`);
return successResponse(data); return successResponse(data);
} catch (error) { } catch (error) {
@ -300,7 +299,7 @@ export class SiteApiController {
} }
} }
// 平台特性产品导出特殊CSV走平台服务 // 平台特性:产品导出(特殊CSV走平台服务)
@Get('/:siteId/links') @Get('/:siteId/links')
async getLinks( async getLinks(
@Param('siteId') siteId: number @Param('siteId') siteId: number
@ -327,7 +326,7 @@ export class SiteApiController {
if (site.type === 'woocommerce') { if (site.type === 'woocommerce') {
const page = query.page || 1; const page = query.page || 1;
const perPage = (query.per_page) || 100; const perPage = (query.per_page) || 100;
const res = await this.siteApiService.wpService.getProducts(site, page, perPage); const res = await this.siteApiService.wpService.getProducts(site, { page, per_page: perPage });
const header = ['id', 'name', 'type', 'status', 'sku', 'regular_price', 'sale_price', 'stock_status', 'stock_quantity']; const header = ['id', 'name', 'type', 'status', 'sku', 'regular_price', 'sale_price', 'stock_status', 'stock_quantity'];
const rows = (res.items || []).map((p: any) => [p.id, p.name, p.type, p.status, p.sku, p.regular_price, p.sale_price, p.stock_status, p.stock_quantity]); const rows = (res.items || []).map((p: any) => [p.id, p.name, p.type, p.status, p.sku, p.regular_price, p.sale_price, p.stock_status, p.stock_quantity]);
const toCsvValue = (val: any) => { const toCsvValue = (val: any) => {
@ -360,7 +359,7 @@ export class SiteApiController {
this.logger.info(`[Site API] 获取单个产品开始, siteId: ${siteId}, productId: ${id}`); this.logger.info(`[Site API] 获取单个产品开始, siteId: ${siteId}, productId: ${id}`);
try { try {
const adapter = await this.siteApiService.getAdapter(siteId); const adapter = await this.siteApiService.getAdapter(siteId);
const data = await adapter.getProduct(id); const data = await adapter.getProduct({ id });
// 如果获取到商品数据则增强ERP产品信息 // 如果获取到商品数据则增强ERP产品信息
if (data) { if (data) {
@ -430,7 +429,7 @@ export class SiteApiController {
} }
} }
// 平台特性产品导入特殊CSV走平台服务 // 平台特性:产品导入(特殊CSV走平台服务)
@Post('/:siteId/products/import-special') @Post('/:siteId/products/import-special')
@ApiOkResponse({ type: Object }) @ApiOkResponse({ type: Object })
async importProductsSpecial( async importProductsSpecial(
@ -444,7 +443,7 @@ export class SiteApiController {
const created: any[] = []; const created: any[] = [];
const failed: any[] = []; const failed: any[] = [];
if (site.type === 'woocommerce') { if (site.type === 'woocommerce') {
// 解析 CSV 为对象数组(若传入 items 则优先 items // 解析 CSV 为对象数组(若传入 items 则优先 items)
let payloads = items; let payloads = items;
if (!payloads.length && csvText) { if (!payloads.length && csvText) {
const lines = csvText.split(/\r?\n/).filter(Boolean); const lines = csvText.split(/\r?\n/).filter(Boolean);
@ -485,7 +484,7 @@ export class SiteApiController {
this.logger.info(`[Site API] 更新产品开始, siteId: ${siteId}, productId: ${id}`); this.logger.info(`[Site API] 更新产品开始, siteId: ${siteId}, productId: ${id}`);
try { try {
const adapter = await this.siteApiService.getAdapter(siteId); const adapter = await this.siteApiService.getAdapter(siteId);
const data = await adapter.updateProduct(id, body); const data = await adapter.updateProduct({ id }, body);
this.logger.info(`[Site API] 更新产品成功, siteId: ${siteId}, productId: ${id}`); this.logger.info(`[Site API] 更新产品成功, siteId: ${siteId}, productId: ${id}`);
return successResponse(data); return successResponse(data);
} catch (error) { } catch (error) {
@ -540,7 +539,7 @@ export class SiteApiController {
this.logger.info(`[Site API] 删除产品开始, siteId: ${siteId}, productId: ${id}`); this.logger.info(`[Site API] 删除产品开始, siteId: ${siteId}, productId: ${id}`);
try { try {
const adapter = await this.siteApiService.getAdapter(siteId); const adapter = await this.siteApiService.getAdapter(siteId);
const success = await adapter.deleteProduct(id); const success = await adapter.deleteProduct({ id });
this.logger.info(`[Site API] 删除产品成功, siteId: ${siteId}, productId: ${id}`); this.logger.info(`[Site API] 删除产品成功, siteId: ${siteId}, productId: ${id}`);
return successResponse(success); return successResponse(success);
} catch (error) { } catch (error) {
@ -585,7 +584,7 @@ export class SiteApiController {
for (const item of body.update) { for (const item of body.update) {
try { try {
const id = item.id; const id = item.id;
const data = await adapter.updateProduct(id, item); const data = await adapter.updateProduct({ id }, item);
updated.push(data); updated.push(data);
} catch (e) { } catch (e) {
errors.push({ errors.push({
@ -598,7 +597,7 @@ export class SiteApiController {
if (body.delete?.length) { if (body.delete?.length) {
for (const id of body.delete) { for (const id of body.delete) {
try { try {
const ok = await adapter.deleteProduct(id); const ok = await adapter.deleteProduct({ id });
if (ok) deleted.push(id); if (ok) deleted.push(id);
else errors.push({ else errors.push({
identifier: String(id), identifier: String(id),
@ -672,6 +671,26 @@ export class SiteApiController {
} }
} }
@Get('/:siteId/orders/count')
@ApiOkResponse({ type: Object })
async countOrders(
@Param('siteId') siteId: number,
@Query() query: any
) {
this.logger.info(`[Site API] 获取订单总数开始, siteId: ${siteId}`);
try {
const adapter = await this.siteApiService.getAdapter(siteId);
const total = await adapter.countOrders(query);
this.logger.info(`[Site API] 获取订单总数成功, siteId: ${siteId}, total: ${total}`);
return successResponse({ total });
} catch (error) {
this.logger.error(`[Site API] 获取订单总数失败, siteId: ${siteId}, 错误信息: ${error.message}`);
return errorResponse(error.message);
}
}
@Get('/:siteId/customers/:customerId/orders') @Get('/:siteId/customers/:customerId/orders')
@ApiOkResponse({ type: UnifiedOrderPaginationDTO }) @ApiOkResponse({ type: UnifiedOrderPaginationDTO })
async getCustomerOrders( async getCustomerOrders(
@ -752,7 +771,7 @@ export class SiteApiController {
this.logger.info(`[Site API] 获取单个订单开始, siteId: ${siteId}, orderId: ${id}`); this.logger.info(`[Site API] 获取单个订单开始, siteId: ${siteId}, orderId: ${id}`);
try { try {
const adapter = await this.siteApiService.getAdapter(siteId); const adapter = await this.siteApiService.getAdapter(siteId);
const data = await adapter.getOrder(id); const data = await adapter.getOrder({ id });
this.logger.info(`[Site API] 获取单个订单成功, siteId: ${siteId}, orderId: ${id}`); this.logger.info(`[Site API] 获取单个订单成功, siteId: ${siteId}, orderId: ${id}`);
return successResponse(data); return successResponse(data);
} catch (error) { } catch (error) {
@ -824,7 +843,7 @@ export class SiteApiController {
this.logger.info(`[Site API] 更新订单开始, siteId: ${siteId}, orderId: ${id}`); this.logger.info(`[Site API] 更新订单开始, siteId: ${siteId}, orderId: ${id}`);
try { try {
const adapter = await this.siteApiService.getAdapter(siteId); const adapter = await this.siteApiService.getAdapter(siteId);
const ok = await adapter.updateOrder(id, body); const ok = await adapter.updateOrder({ id }, body);
this.logger.info(`[Site API] 更新订单成功, siteId: ${siteId}, orderId: ${id}`); this.logger.info(`[Site API] 更新订单成功, siteId: ${siteId}, orderId: ${id}`);
return successResponse(ok); return successResponse(ok);
} catch (error) { } catch (error) {
@ -842,7 +861,7 @@ export class SiteApiController {
this.logger.info(`[Site API] 删除订单开始, siteId: ${siteId}, orderId: ${id}`); this.logger.info(`[Site API] 删除订单开始, siteId: ${siteId}, orderId: ${id}`);
try { try {
const adapter = await this.siteApiService.getAdapter(siteId); const adapter = await this.siteApiService.getAdapter(siteId);
const ok = await adapter.deleteOrder(id); const ok = await adapter.deleteOrder({ id });
this.logger.info(`[Site API] 删除订单成功, siteId: ${siteId}, orderId: ${id}`); this.logger.info(`[Site API] 删除订单成功, siteId: ${siteId}, orderId: ${id}`);
return successResponse(ok); return successResponse(ok);
} catch (error) { } catch (error) {
@ -882,7 +901,7 @@ export class SiteApiController {
for (const item of body.update) { for (const item of body.update) {
try { try {
const id = item.id; const id = item.id;
const ok = await adapter.updateOrder(id, item); const ok = await adapter.updateOrder({ id }, item);
if (ok) updated.push(item); if (ok) updated.push(item);
else errors.push({ else errors.push({
identifier: String(item.id || 'unknown'), identifier: String(item.id || 'unknown'),
@ -899,7 +918,7 @@ export class SiteApiController {
if (body.delete?.length) { if (body.delete?.length) {
for (const id of body.delete) { for (const id of body.delete) {
try { try {
const ok = await adapter.deleteOrder(id); const ok = await adapter.deleteOrder({ id });
if (ok) deleted.push(id); if (ok) deleted.push(id);
else errors.push({ else errors.push({
identifier: String(id), identifier: String(id),
@ -966,25 +985,6 @@ export class SiteApiController {
} }
} }
@Post('/:siteId/orders/:id/fulfill')
@ApiOkResponse({ type: Object })
async fulfillOrder(
@Param('siteId') siteId: number,
@Param('id') id: string,
@Body() body: FulfillmentDTO
) {
this.logger.info(`[Site API] 订单履约开始, siteId: ${siteId}, orderId: ${id}`);
try {
const adapter = await this.siteApiService.getAdapter(siteId);
const data = await adapter.fulfillOrder(id, body);
this.logger.info(`[Site API] 订单履约成功, siteId: ${siteId}, orderId: ${id}`);
return successResponse(data);
} catch (error) {
this.logger.error(`[Site API] 订单履约失败, siteId: ${siteId}, orderId: ${id}, 错误信息: ${error.message}`);
return errorResponse(error.message);
}
}
@Post('/:siteId/orders/:id/cancel-fulfill') @Post('/:siteId/orders/:id/cancel-fulfill')
@ApiOkResponse({ type: Object }) @ApiOkResponse({ type: Object })
async cancelFulfillment( async cancelFulfillment(
@ -1050,13 +1050,13 @@ export class SiteApiController {
} }
} }
@Get('/:siteId/orders/:orderId/trackings') @Get('/:siteId/orders/:orderId/fulfillments')
@ApiOkResponse({ type: Object }) @ApiOkResponse({ type: Object })
async getOrderTrackings( async getOrderFulfillments(
@Param('siteId') siteId: number, @Param('siteId') siteId: number,
@Param('orderId') orderId: string @Param('orderId') orderId: string
) { ) {
this.logger.info(`[Site API] 获取订单物流跟踪信息开始, siteId: ${siteId}, orderId: ${orderId}`); this.logger.info(`[Site API] 获取订单履约信息开始, siteId: ${siteId}, orderId: ${orderId}`);
try { try {
const adapter = await this.siteApiService.getAdapter(siteId); const adapter = await this.siteApiService.getAdapter(siteId);
const data = await adapter.getOrderFulfillments(orderId); const data = await adapter.getOrderFulfillments(orderId);
@ -1435,7 +1435,7 @@ export class SiteApiController {
this.logger.info(`[Site API] 获取单个客户开始, siteId: ${siteId}, customerId: ${id}`); this.logger.info(`[Site API] 获取单个客户开始, siteId: ${siteId}, customerId: ${id}`);
try { try {
const adapter = await this.siteApiService.getAdapter(siteId); const adapter = await this.siteApiService.getAdapter(siteId);
const data = await adapter.getCustomer(id); const data = await adapter.getCustomer({ id });
this.logger.info(`[Site API] 获取单个客户成功, siteId: ${siteId}, customerId: ${id}`); this.logger.info(`[Site API] 获取单个客户成功, siteId: ${siteId}, customerId: ${id}`);
return successResponse(data); return successResponse(data);
} catch (error) { } catch (error) {
@ -1507,7 +1507,7 @@ export class SiteApiController {
this.logger.info(`[Site API] 更新客户开始, siteId: ${siteId}, customerId: ${id}`); this.logger.info(`[Site API] 更新客户开始, siteId: ${siteId}, customerId: ${id}`);
try { try {
const adapter = await this.siteApiService.getAdapter(siteId); const adapter = await this.siteApiService.getAdapter(siteId);
const data = await adapter.updateCustomer(id, body); const data = await adapter.updateCustomer({ id }, body);
this.logger.info(`[Site API] 更新客户成功, siteId: ${siteId}, customerId: ${id}`); this.logger.info(`[Site API] 更新客户成功, siteId: ${siteId}, customerId: ${id}`);
return successResponse(data); return successResponse(data);
} catch (error) { } catch (error) {
@ -1525,7 +1525,7 @@ export class SiteApiController {
this.logger.info(`[Site API] 删除客户开始, siteId: ${siteId}, customerId: ${id}`); this.logger.info(`[Site API] 删除客户开始, siteId: ${siteId}, customerId: ${id}`);
try { try {
const adapter = await this.siteApiService.getAdapter(siteId); const adapter = await this.siteApiService.getAdapter(siteId);
const success = await adapter.deleteCustomer(id); const success = await adapter.deleteCustomer({ id });
this.logger.info(`[Site API] 删除客户成功, siteId: ${siteId}, customerId: ${id}`); this.logger.info(`[Site API] 删除客户成功, siteId: ${siteId}, customerId: ${id}`);
return successResponse(success); return successResponse(success);
} catch (error) { } catch (error) {
@ -1561,7 +1561,7 @@ export class SiteApiController {
for (const item of body.update) { for (const item of body.update) {
try { try {
const id = item.id; const id = item.id;
const data = await adapter.updateCustomer(id, item); const data = await adapter.updateCustomer({ id }, item);
updated.push(data); updated.push(data);
} catch (e) { } catch (e) {
failed.push({ action: 'update', item, error: (e as any).message }); failed.push({ action: 'update', item, error: (e as any).message });
@ -1571,7 +1571,7 @@ export class SiteApiController {
if (body.delete?.length) { if (body.delete?.length) {
for (const id of body.delete) { for (const id of body.delete) {
try { try {
const ok = await adapter.deleteCustomer(id); const ok = await adapter.deleteCustomer({ id });
if (ok) deleted.push(id); if (ok) deleted.push(id);
else failed.push({ action: 'delete', id, error: 'delete failed' }); else failed.push({ action: 'delete', id, error: 'delete failed' });
} catch (e) { } catch (e) {

View File

@ -96,7 +96,7 @@ export class WebhookController {
break; break;
case 'order.created': case 'order.created':
case 'order.updated': case 'order.updated':
const order = adapter.mapOrder(body) const order = adapter.mapPlatformToUnifiedOrder(body)
await this.orderService.syncSingleOrder(siteId, order); await this.orderService.syncSingleOrder(siteId, order);
break; break;
case 'order.deleted': case 'order.deleted':
@ -162,7 +162,7 @@ export class WebhookController {
break; break;
case 'orders/create': case 'orders/create':
case 'orders/update': case 'orders/update':
const order = adapter.mapOrder(body) const order = adapter.mapPlatformToUnifiedOrder(body)
await this.orderService.syncSingleOrder(siteId, order); await this.orderService.syncSingleOrder(siteId, order);
break; break;
case 'orders/delete': case 'orders/delete':

View File

@ -126,7 +126,7 @@ const flavorsData = [
{ name: 'arctic-mint', title: 'arctic mint', titleCn: '北极薄荷', shortName: 'AR' }, { name: 'arctic-mint', title: 'arctic mint', titleCn: '北极薄荷', shortName: 'AR' },
{ name: 'baddie-blueberries', title: 'baddie blueberries', titleCn: '时髦蓝莓', shortName: 'BA' }, { name: 'baddie-blueberries', title: 'baddie blueberries', titleCn: '时髦蓝莓', shortName: 'BA' },
{ name: 'banana', title: 'banana', 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', title: 'banana berry', titleCn: '香蕉莓果', shortName: 'BA' },
{ name: 'banana-berry-melon-ice', title: 'banana berry melon ice', 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' }, { 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: '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: 'berries-in-the-6ix', title: 'berries in the 6ix', titleCn: '多伦多莓果', shortName: 'BE' },
{ name: 'berry-burst', title: 'berry burst', 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-ice', title: 'berry ice', titleCn: '浆果冰', shortName: 'BE' },
{ name: 'berry-lime-ice', title: 'berry lime 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' }, { 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: 'black-cherry', title: 'black cherry', titleCn: '黑樱桃', shortName: 'BL' },
{ name: 'blackcherry', title: 'blackcherry', titleCn: '黑樱桃混合', shortName: 'BL' }, { name: 'blackcherry', title: 'blackcherry', titleCn: '黑樱桃混合', shortName: 'BL' },
{ name: 'blackcurrant-ice', title: 'blackcurrant ice', 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-licorice', title: 'black licorice', titleCn: '黑甘草', shortName: 'BL' },
{ name: 'black-tea', title: 'black tea', titleCn: '红茶', shortName: 'BL' }, { name: 'black-tea', title: 'black tea', titleCn: '红茶', shortName: 'BL' },
{ name: 'blackberry-ice', title: 'blackberry ice', 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', title: 'blue razz', titleCn: '蓝覆盆子', shortName: 'BL' },
{ name: 'blue-razz-hype', title: 'blue razz hype', 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', 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-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-lemon-ice', title: 'blue razz lemon ice', titleCn: '蓝覆盆子柠檬冰', shortName: 'BL' },
{ name: 'blue-razz-lemonade', title: 'blue razz lemonade', 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: 'bumpin-blackcurrant-iced', title: 'bumpin blackcurrant iced', titleCn: '黑加仑热烈冰', shortName: 'BU' },
{ name: 'burst-ice', title: 'burst ice', 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: 'bussin-banana-iced', title: 'bussin banana iced', titleCn: '香蕉热烈冰(重复)', shortName: 'BU' },
{ name: 'california-cherry', title: 'california cherry', titleCn: '加州樱桃', shortName: 'CA' }, { name: 'california-cherry', title: 'california cherry', titleCn: '加州樱桃', shortName: 'CA' },
{ name: 'cantaloupe-mango-banana', title: 'cantaloupe mango banana', titleCn: '香瓜芒果香蕉', shortName: 'CA' }, { name: 'cantaloupe-mango-banana', title: 'cantaloupe mango banana', titleCn: '香瓜芒果香蕉', shortName: 'CA' },
{ name: 'caramel', title: 'caramel', 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-chill', title: 'citrus chill', titleCn: '柑橘清凉', shortName: 'CI' },
{ name: 'citrus-smash-ice', title: 'citrus smash ice', 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', 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', title: 'classic', titleCn: '经典', shortName: 'CL' },
{ name: 'classic-ice', title: 'classic ice', titleCn: '经典冰', shortName: 'CL' }, { name: 'classic-ice', title: 'classic ice', titleCn: '经典冰', shortName: 'CL' },
{ name: 'classic-mint-ice', title: 'classic mint 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: 'fizzy', title: 'fizzy', titleCn: '汽水', shortName: 'FI' },
{ name: 'flavourless', title: 'flavourless', titleCn: '无味', shortName: 'FL' }, { name: 'flavourless', title: 'flavourless', titleCn: '无味', shortName: 'FL' },
{ name: 'flippin-fruit-flash', title: 'flippin fruit flash', 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: 'forest-fruits', title: 'forest fruits', titleCn: '森林水果', shortName: 'FO' },
{ name: 'fragrant-grapefruit', title: 'fragrant grapefruit', titleCn: '香气葡萄柚', shortName: 'FR' }, { name: 'fragrant-grapefruit', title: 'fragrant grapefruit', titleCn: '香气葡萄柚', shortName: 'FR' },
{ name: 'freeze', title: 'freeze', 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: 'fuji-melon-ice', title: 'fuji melon ice', titleCn: '富士瓜冰', shortName: 'FU' },
{ name: 'full-charge', title: 'full charge', titleCn: '满电', shortName: 'FU' }, { name: 'full-charge', title: 'full charge', titleCn: '满电', shortName: 'FU' },
{ name: 'gb', title: 'gb', titleCn: '软糖', shortName: 'GB' }, { 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: 'gentle-mint', title: 'gentle mint', titleCn: '温和薄荷', shortName: 'GE' },
{ name: 'ghost-cola-&-vanilla', title: 'ghost cola & vanilla', titleCn: '幽灵可乐香草', shortName: 'GH' }, { name: 'ghost-cola-&-vanilla', title: 'ghost cola & vanilla', titleCn: '幽灵可乐香草', shortName: 'GH' },
{ name: 'ghost-cola-ice', title: 'ghost cola ice', 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-mango', title: 'ghost mango', titleCn: '幽灵芒果', shortName: 'GH' },
{ name: 'ghost-original', title: 'ghost original', titleCn: '幽灵原味', shortName: 'GH' }, { name: 'ghost-original', title: 'ghost original', titleCn: '幽灵原味', shortName: 'GH' },
{ name: 'ghost-watermelon-ice', title: 'ghost watermelon ice', 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: 'gold-edition', title: 'gold edition', titleCn: '金版', shortName: 'GO' },
{ name: 'grape', title: 'grape', titleCn: '葡萄', shortName: 'GR' }, { name: 'grape', title: 'grape', titleCn: '葡萄', shortName: 'GR' },
{ name: 'grape-cherry', title: 'grape cherry', 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: 'mixed-fruit', title: 'mixed fruit', titleCn: '混合水果', shortName: 'MI' },
{ name: 'mocha-ice', title: 'mocha ice', titleCn: '摩卡冰', shortName: 'MO' }, { name: 'mocha-ice', title: 'mocha ice', titleCn: '摩卡冰', shortName: 'MO' },
{ name: 'morocco-mint', title: 'morocco mint', 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: 'mung-beans', title: 'mung beans', titleCn: '绿豆', shortName: 'MU' },
{ name: 'nasty-tropic', title: 'nasty tropic', titleCn: '恶搞热带', shortName: 'NA' }, { name: 'nasty-tropic', title: 'nasty tropic', titleCn: '恶搞热带', shortName: 'NA' },
{ name: 'nectarine-ice', title: 'nectarine ice', titleCn: '油桃冰', shortName: 'NE' }, { name: 'nectarine-ice', title: 'nectarine ice', titleCn: '油桃冰', shortName: 'NE' },
{ name: 'night-rider', title: 'night rider', titleCn: '夜骑', shortName: 'NI' }, { name: 'night-rider', title: 'night rider', titleCn: '夜骑', shortName: 'NI' },
{ name: 'nirvana', title: 'nirvana', 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: 'northern-blue-razz', title: 'northern blue razz', titleCn: '北方蓝覆盆子', shortName: 'NO' },
{ name: 'nutty-virginia', title: 'nutty virginia', titleCn: '坚果弗吉尼亚', shortName: 'NU' }, { name: 'nutty-virginia', title: 'nutty virginia', titleCn: '坚果弗吉尼亚', shortName: 'NU' },
{ name: 'orange', title: 'orange', titleCn: '橙子', shortName: 'OR' }, { 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-guava', title: 'orange mango guava', titleCn: '橙子芒果番石榴', shortName: 'OR' },
{ name: 'orange-mango-pineapple-ice', title: 'orange mango pineapple ice', 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', 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-spark', title: 'orange spark', titleCn: '橙色火花', shortName: 'OR' },
{ name: 'orange-tangerine', title: 'orange tangerine', titleCn: '橙子柑橘', shortName: 'OR' }, { name: 'orange-tangerine', title: 'orange tangerine', titleCn: '橙子柑橘', shortName: 'OR' },
{ name: 'original', title: 'original', 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', title: 'packin peach berry', titleCn: '装满桃浆果', shortName: 'PA' },
{ name: 'packin-peach-berry-(popn-peach-berry)', title: 'packin peach berry (popn peach berry)', titleCn: '装满桃浆果Popn 桃浆果)', shortName: 'PA' }, { name: 'packin-peach-berry-(popn-peach-berry)', title: 'packin peach berry (popn peach berry)', titleCn: '装满桃浆果(Popn 桃浆果)', shortName: 'PA' },
{ name: 'papio', title: 'papio', titleCn: 'Papio', shortName: 'PA' }, { name: 'papio', title: 'papio', titleCn: 'Papio', shortName: 'PA' },
{ name: 'paradise', title: 'paradise', titleCn: '天堂', shortName: 'PA' }, { name: 'paradise', title: 'paradise', titleCn: '天堂', shortName: 'PA' },
{ name: 'paradise-iced', title: 'paradise iced', 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-fruits', title: 'red fruits', titleCn: '红色水果', shortName: 'RE' },
{ name: 'red-lightning', title: 'red lightning', titleCn: '红色闪电', shortName: 'RE' }, { name: 'red-lightning', title: 'red lightning', titleCn: '红色闪电', shortName: 'RE' },
{ name: 'red-line', title: 'red line', 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: 'red-magic', title: 'red magic', titleCn: '红魔', shortName: 'RE' },
{ name: 'rich-tobacco', title: 'rich tobacco', titleCn: '浓烈烟草', shortName: 'RI' }, { name: 'rich-tobacco', title: 'rich tobacco', titleCn: '浓烈烟草', shortName: 'RI' },
{ name: 'root-beer', title: 'root beer', titleCn: '根啤', shortName: 'RO' }, { 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: 'sic-strawberry-iced', title: 'sic strawberry iced', titleCn: '意大利草莓冰', shortName: 'SI' },
{ name: 'simply-spearmint', title: 'simply spearmint', titleCn: '清爽留兰香', shortName: 'SI' }, { name: 'simply-spearmint', title: 'simply spearmint', titleCn: '清爽留兰香', shortName: 'SI' },
{ name: 'skc', title: 'skc', titleCn: 'SKC', shortName: 'SK' }, { name: 'skc', title: 'skc', titleCn: 'SKC', shortName: 'SK' },
{ name: 'skc(skittles-candy)', title: 'skc(skittles candy)', 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: '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: 'slammin-sts-iced', title: 'slammin sts iced', titleCn: '热烈 STS 冰', shortName: 'SL' },
{ name: 'smooth', title: 'smooth', titleCn: '顺滑', shortName: 'SM' }, { name: 'smooth', title: 'smooth', titleCn: '顺滑', shortName: 'SM' },
{ name: 'smooth-mint', title: 'smooth mint', 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-t', title: 'strawberry jasmine t', titleCn: '草莓茉莉茶', shortName: 'ST' },
{ name: 'strawberry-jasmine-tea', title: 'strawberry jasmine tea', 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', 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-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-guava-ice', title: 'strawberry kiwi guava ice', titleCn: '草莓奇异果番石榴冰', shortName: 'ST' },
{ name: 'strawberry-kiwi-ice', title: 'strawberry kiwi 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', title: 'strawberry watermelon', titleCn: '草莓西瓜', shortName: 'ST' },
{ name: 'strawberry-watermelon-ice', title: 'strawberry watermelon ice', 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', 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: 'strawnana-orange', title: 'strawnana orange', titleCn: '草莓香蕉橙', shortName: 'ST' },
{ name: 'summer-grape', title: 'summer grape', titleCn: '夏日葡萄', shortName: 'SU' }, { 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-sour-blueberry-iced', title: 'super sour blueberry iced', titleCn: '超级酸蓝莓冰', shortName: 'SU' },
{ name: 'super-spearmint', title: 'super spearmint', titleCn: '超级留兰香', shortName: 'SU' }, { name: 'super-spearmint', title: 'super spearmint', titleCn: '超级留兰香', shortName: 'SU' },
{ name: 'super-spearmint-iced', title: 'super spearmint iced', 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-orang-ice', title: 'tropical orang ice', titleCn: '热带橙冰', shortName: 'TR' },
{ name: 'tropical-prism-blast', title: 'tropical prism blast', 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', 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-storm-ice', title: 'tropical storm ice', titleCn: '热带风暴冰', shortName: 'TR' },
{ name: 'tropical-summer', title: 'tropical summer', titleCn: '热带夏日', shortName: 'TR' }, { name: 'tropical-summer', title: 'tropical summer', titleCn: '热带夏日', shortName: 'TR' },
{ name: 'tropika', title: 'tropika', 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-cantaloupe-honeydew-ice', title: 'watermelon cantaloupe honeydew ice', titleCn: '西瓜香瓜蜜瓜冰', shortName: 'WA' },
{ name: 'watermelon-g', title: 'watermelon g', titleCn: '西瓜 G', shortName: 'WA' }, { name: 'watermelon-g', title: 'watermelon g', titleCn: '西瓜 G', shortName: 'WA' },
{ name: 'watermelon-ice', title: 'watermelon ice', titleCn: '西瓜冰', 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-lime-ice', title: 'watermelon lime ice', titleCn: '西瓜青柠冰', shortName: 'WA' },
{ name: 'watermelon-mango-tango', title: 'watermelon mango tango', titleCn: '西瓜芒果探戈', shortName: 'WA' }, { name: 'watermelon-mango-tango', title: 'watermelon mango tango', titleCn: '西瓜芒果探戈', shortName: 'WA' },
{ name: 'watermelona-cg', title: 'watermelona cg', titleCn: '西瓜 CG', 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-strawberry-watermelon', title: 'wild strawberry watermelon', titleCn: '野生草莓西瓜', shortName: 'WI' },
{ name: 'wild-white-grape', title: 'wild white grape', 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-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-berry-ice', title: 'winter berry ice', titleCn: '冬季浆果冰', shortName: 'WI' },
{ name: 'winter-green', title: 'winter green', titleCn: '冬青', shortName: 'WI' }, { name: 'winter-green', title: 'winter green', titleCn: '冬青', shortName: 'WI' },
{ name: 'wintergreen', title: 'wintergreen', titleCn: '冬青薄荷', shortName: 'WI' }, { name: 'wintergreen', title: 'wintergreen', titleCn: '冬青薄荷', shortName: 'WI' },

View File

@ -23,19 +23,38 @@ export default class TemplateSeeder implements Seeder {
const templates = [ const templates = [
{ {
name: 'product.sku', name: 'product.sku',
value: "<%= [it.category.shortName].concat(it.attributes.map(a => a.shortName)).join('-') %>", value: `<%
// 按分类判断属性排序逻辑
if (it.category.name === 'nicotine-pouches') {
// 1. 定义 nicotine-pouches 专属的属性固定顺序
const fixedOrder = ['brand','category', 'flavor', 'strength', 'humidity'];
sortedAttrShortNames = fixedOrder.map(attrKey => {
if(attrKey === 'category') return it.category.shortName
// 排序
const matchedAttr = it.attributes.find(a => a?.dict?.name === attrKey);
return matchedAttr ? matchedAttr.shortName : '';
}).filter(Boolean); // 移除空值,避免多余的 "-"
} else {
// 非目标分类,保留 attributes 原有顺序
sortedAttrShortNames = it.attributes.map(a => a.shortName);
}
// 4. 拼接分类名 + 排序后的属性名
%><%= sortedAttrShortNames.join('-') %><%
%>`,
description: '产品SKU模板', description: '产品SKU模板',
testData: JSON.stringify({ testData: JSON.stringify({
category: { "category": {
shortName: 'CAT', "name": "nicotine-pouches",
"shortName": "NP"
}, },
attributes: [ "attributes": [
{ shortName: 'BR' }, { "dict": {"name": "brand"},"shortName": "YOONE" },
{ shortName: 'FL' }, { "dict": {"name": "flavor"},"shortName": "FL" },
{ shortName: '10MG' }, { "dict": {"name": "strength"},"shortName": "10MG" },
{ shortName: 'DRY' }, { "dict": {"name": "humidity"},"shortName": "DRY" }
], ]
}), }),
}, },
{ {
name: 'product.title', name: 'product.title',

View File

@ -56,7 +56,7 @@ export class UnifiedSearchParamsDTO<Where=Record<string, any>> {
* *
*/ */
export interface BatchErrorItem { export interface BatchErrorItem {
// 错误项标识可以是ID、邮箱等 // 错误项标识(可以是ID、邮箱等)
identifier: string; identifier: string;
// 错误信息 // 错误信息
error: string; error: string;
@ -76,7 +76,7 @@ export interface BatchOperationResult {
updated?: number; updated?: number;
// 删除数量 // 删除数量
deleted?: number; deleted?: number;
// 跳过的数量(如数据已存在或无需处理) // 跳过的数量(如数据已存在或无需处理)
skipped?: number; skipped?: number;
// 错误列表 // 错误列表
errors: BatchErrorItem[]; errors: BatchErrorItem[];
@ -101,7 +101,7 @@ export class SyncOperationResult implements BatchOperationResult {
* DTO * DTO
*/ */
export class BatchErrorItemDTO { export class BatchErrorItemDTO {
@ApiProperty({ description: '错误项标识如ID、邮箱等', type: String }) @ApiProperty({ description: '错误项标识(如ID、邮箱等)', type: String })
@Rule(RuleType.string().required()) @Rule(RuleType.string().required())
identifier: string; identifier: string;
@ -164,7 +164,7 @@ export class SyncParamsDTO {
@Rule(RuleType.string().optional()) @Rule(RuleType.string().optional())
endDate?: string; endDate?: string;
@ApiProperty({ description: '强制同步(忽略缓存)', type: Boolean, required: false, default: false }) @ApiProperty({ description: '强制同步(忽略缓存)', type: Boolean, required: false, default: false })
@Rule(RuleType.boolean().optional()) @Rule(RuleType.boolean().optional())
force?: boolean = false; force?: boolean = false;
} }
@ -183,7 +183,7 @@ export class BatchQueryDTO {
} }
/** /**
* * ()
*/ */
export class BatchOperationResultDTOGeneric<T> extends BatchOperationResultDTO { export class BatchOperationResultDTOGeneric<T> extends BatchOperationResultDTO {
@ApiProperty({ description: '操作成功的数据列表', type: Array }) @ApiProperty({ description: '操作成功的数据列表', type: Array })
@ -191,7 +191,7 @@ export class BatchOperationResultDTOGeneric<T> extends BatchOperationResultDTO {
} }
/** /**
* * ()
*/ */
export class SyncOperationResultDTOGeneric<T> extends SyncOperationResultDTO { export class SyncOperationResultDTOGeneric<T> extends SyncOperationResultDTO {
@ApiProperty({ description: '同步成功的数据列表', type: Array }) @ApiProperty({ description: '同步成功的数据列表', type: Array })

View File

@ -5,7 +5,7 @@ import { Rule, RuleType } from '@midwayjs/validate';
* *
*/ */
export interface BatchErrorItem { export interface BatchErrorItem {
// 错误项标识可以是ID、邮箱等 // 错误项标识(可以是ID、邮箱等)
identifier: string; identifier: string;
// 错误信息 // 错误信息
error: string; error: string;
@ -25,7 +25,7 @@ export interface BatchOperationResult {
updated?: number; updated?: number;
// 删除数量 // 删除数量
deleted?: number; deleted?: number;
// 跳过的数量(如数据已存在或无需处理) // 跳过的数量(如数据已存在或无需处理)
skipped?: number; skipped?: number;
// 错误列表 // 错误列表
errors: BatchErrorItem[]; errors: BatchErrorItem[];
@ -43,7 +43,7 @@ export interface SyncOperationResult extends BatchOperationResult {
* DTO * DTO
*/ */
export class BatchErrorItemDTO { export class BatchErrorItemDTO {
@ApiProperty({ description: '错误项标识如ID、邮箱等', type: String }) @ApiProperty({ description: '错误项标识(如ID、邮箱等)', type: String })
@Rule(RuleType.string().required()) @Rule(RuleType.string().required())
identifier: string; identifier: string;
@ -114,7 +114,7 @@ export class BatchDeleteDTO {
} }
/** /**
* DTO * DTO()
*/ */
export class BatchOperationDTO<T = any> { export class BatchOperationDTO<T = any> {
@ApiProperty({ description: '要创建的数据列表', type: Array, required: false }) @ApiProperty({ description: '要创建的数据列表', type: Array, required: false })
@ -175,7 +175,7 @@ export class SyncParamsDTO {
@Rule(RuleType.string().optional()) @Rule(RuleType.string().optional())
endDate?: string; endDate?: string;
@ApiProperty({ description: '强制同步(忽略缓存)', type: Boolean, required: false, default: false }) @ApiProperty({ description: '强制同步(忽略缓存)', type: Boolean, required: false, default: false })
@Rule(RuleType.boolean().optional()) @Rule(RuleType.boolean().optional())
force?: boolean = false; force?: boolean = false;
} }
@ -194,7 +194,7 @@ export class BatchQueryDTO {
} }
/** /**
* * ()
*/ */
export class BatchOperationResultDTOGeneric<T> extends BatchOperationResultDTO { export class BatchOperationResultDTOGeneric<T> extends BatchOperationResultDTO {
@ApiProperty({ description: '操作成功的数据列表', type: Array }) @ApiProperty({ description: '操作成功的数据列表', type: Array })
@ -202,7 +202,7 @@ export class BatchOperationResultDTOGeneric<T> extends BatchOperationResultDTO {
} }
/** /**
* * ()
*/ */
export class SyncOperationResultDTOGeneric<T> extends SyncOperationResultDTO { export class SyncOperationResultDTOGeneric<T> extends SyncOperationResultDTO {
@ApiProperty({ description: '同步成功的数据列表', type: Array }) @ApiProperty({ description: '同步成功的数据列表', type: Array })

View File

@ -2,7 +2,7 @@ import { ApiProperty } from '@midwayjs/swagger';
import { UnifiedSearchParamsDTO } from './api.dto'; import { UnifiedSearchParamsDTO } from './api.dto';
import { Customer } from '../entity/customer.entity'; import { Customer } from '../entity/customer.entity';
// 客户基本信息DTO(用于响应) // 客户基本信息DTO(用于响应)
export class CustomerDTO extends Customer{ export class CustomerDTO extends Customer{
@ApiProperty({ description: '客户ID' }) @ApiProperty({ description: '客户ID' })
id: number; id: number;
@ -163,11 +163,11 @@ export class UpdateCustomerDTO {
tags?: string[]; tags?: string[];
} }
// 查询单个客户响应DTO(继承基本信息) // 查询单个客户响应DTO(继承基本信息)
export class GetCustomerDTO extends CustomerDTO { export class GetCustomerDTO extends CustomerDTO {
// 可以添加额外的详细信息字段 // 可以添加额外的详细信息字段
} }
// 客户统计信息DTO(包含订单统计) // 客户统计信息DTO(包含订单统计)
export class CustomerStatisticDTO extends CustomerDTO { export class CustomerStatisticDTO extends CustomerDTO {
@ApiProperty({ description: '创建日期' }) @ApiProperty({ description: '创建日期' })
date_created: Date; date_created: Date;
@ -209,7 +209,7 @@ export class CustomerStatisticWhereDTO {
customerId?: number; customerId?: number;
} }
// 客户统计查询参数DTO(继承通用查询参数) // 客户统计查询参数DTO(继承通用查询参数)
export type CustomerStatisticQueryParamsDTO = UnifiedSearchParamsDTO<CustomerStatisticWhereDTO>; export type CustomerStatisticQueryParamsDTO = UnifiedSearchParamsDTO<CustomerStatisticWhereDTO>;
// 客户统计列表响应DTO // 客户统计列表响应DTO
@ -259,7 +259,7 @@ export class BatchDeleteCustomerDTO {
// ====================== 查询操作 ====================== // ====================== 查询操作 ======================
// 客户查询条件DTO用于UnifiedSearchParamsDTO的where参数 // 客户查询条件DTO(用于UnifiedSearchParamsDTO的where参数)
export class CustomerWhereDTO { export class CustomerWhereDTO {
@ApiProperty({ description: '邮箱筛选', required: false }) @ApiProperty({ description: '邮箱筛选', required: false })
email?: string; email?: string;
@ -284,10 +284,10 @@ export class CustomerWhereDTO {
role?: string; role?: string;
} }
// 客户查询参数DTO(继承通用查询参数) // 客户查询参数DTO(继承通用查询参数)
export type CustomerQueryParamsDTO = UnifiedSearchParamsDTO<CustomerWhereDTO>; export type CustomerQueryParamsDTO = UnifiedSearchParamsDTO<CustomerWhereDTO>;
// 客户列表响应DTO参考site-api.dto.ts中的分页格式 // 客户列表响应DTO(参考site-api.dto.ts中的分页格式)
export class CustomerListResponseDTO { export class CustomerListResponseDTO {
@ApiProperty({ description: '客户列表', type: [CustomerDTO] }) @ApiProperty({ description: '客户列表', type: [CustomerDTO] })
items: CustomerDTO[]; items: CustomerDTO[];
@ -359,6 +359,6 @@ export class SyncCustomersDTO {
@ApiProperty({ description: '站点ID' }) @ApiProperty({ description: '站点ID' })
siteId: number; siteId: number;
@ApiProperty({ description: '查询参数支持where和orderBy', type: UnifiedSearchParamsDTO, required: false }) @ApiProperty({ description: '查询参数(支持where和orderBy)', type: UnifiedSearchParamsDTO, required: false })
params?: UnifiedSearchParamsDTO<CustomerWhereDTO>; params?: UnifiedSearchParamsDTO<CustomerWhereDTO>;
} }

View File

@ -4,6 +4,53 @@ export interface ShopyyTag {
id?: number; id?: number;
name?: string; name?: string;
} }
export interface ShopyyProductQuery {
page: string;
limit: string;
}
/**
* Shopyy
* Shopyy
* 参考文档: https://www.apizza.net/project/e114fb8e628e0f604379f5b26f0d8330/browse
*/
export class ShopyyAllProductQuery {
/** 分页大小,限制返回的商品数量 */
limit?: string;
/** 起始ID用于分页返回ID大于该值的商品 */
since_id?: string;
/** 商品ID精确匹配单个商品 */
id?: string;
/** 商品标题,支持模糊查询 */
title?: string;
/** 商品状态,例如:上架、下架、删除等(具体值参考 Shopyy 接口文档) */
status?: string;
/** 商品SKU编码库存保有单位精确或模糊匹配 */
sku?: string;
/** 商品SPU编码标准化产品单元用于归类同款商品 */
spu?: string;
/** 商品分类ID筛选指定分类下的商品 */
collection_id?: string;
/** 变体价格最小值,筛选变体价格大于等于该值的商品 */
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) */
created_at_min?: string;
/** 商品创建时间最大值,格式参考接口文档(如:YYYY-MM-DD HH:mm:ss) */
created_at_max?: string;
/** 商品更新时间最小值,格式参考接口文档(如:YYYY-MM-DD HH:mm:ss) */
updated_at_min?: string;
/** 商品更新时间最大值,格式参考接口文档(如:YYYY-MM-DD HH:mm:ss) */
updated_at_max?: string;
}
// 产品类型 // 产品类型
export interface ShopyyProduct { export interface ShopyyProduct {
// 产品主键 // 产品主键
@ -83,6 +130,42 @@ export interface ShopyyVariant {
position?: number | string; position?: number | string;
sku_code?: string; sku_code?: string;
} }
//
// 订单查询参数类型
export interface ShopyyOrderQuery {
// 订单ID集合 多个ID用','联接 例:1,2,3
ids?: string;
// 订单状态 100 未完成110 待处理180 已完成(确认收货); 190 取消;
status?: string;
// 物流状态 300 未发货310 部分发货320 已发货330(确认收货)
fulfillment_status?: string;
// 支付状态 200 待支付210 支付中220 部分支付230 已支付240 支付失败250 部分退款260 已退款 290 已取消;
financial_status?: string;
// 支付时间 下限值
pay_at_min?: string;
// 支付时间 上限值
pay_at_max?: string;
// 创建开始时间
created_at_min?: number;
// 创建结束时间
created_at_max?: number;
// 更新时间开始
updated_at_min?: string;
// 更新时间结束
updated_at_max?: string;
// 起始ID
since_id?: string;
// 页码
page?: string;
// 每页条数
limit?: string;
// 排序字段(默认id) id=订单ID updated_at=最后更新时间 pay_at=支付时间
order_field?: string;
// 排序方式(默认desc) desc=降序 asc=升序
order_by?: string;
// 订单列表类型
group?: string;
}
// 订单类型 // 订单类型
export interface ShopyyOrder { export interface ShopyyOrder {
@ -219,7 +302,8 @@ export interface ShopyyOrder {
// 创建时间 // 创建时间
created_at?: number; created_at?: number;
// 发货商品表 id // 发货商品表 id
id?: number }>; id?: number
}>;
}>; }>;
shipping_zone_plans?: Array<{ shipping_zone_plans?: Array<{
shipping_price?: number | string; shipping_price?: number | string;
@ -429,7 +513,7 @@ export class ShopyyFulfillmentDTO {
"tracking_number": string; "tracking_number": string;
"courier_code": number; "courier_code": number;
"note": string; "note": string;
"mode": "replace" | 'cover' | null// 模式 replace(替换) cover (覆盖) 空(新增) "mode": "replace" | 'cover' | null// 模式 replace(替换) cover (覆盖) 空(新增)
} }
// https://www.apizza.net/project/e114fb8e628e0f604379f5b26f0d8330/browse // https://www.apizza.net/project/e114fb8e628e0f604379f5b26f0d8330/browse
export class ShopyPartFulfillmentDTO { export class ShopyPartFulfillmentDTO {

View File

@ -2,6 +2,7 @@ import { ApiProperty } from '@midwayjs/swagger';
import { import {
UnifiedPaginationDTO, UnifiedPaginationDTO,
} from './api.dto'; } from './api.dto';
import { Dict } from '../entity/dict.entity';
// export class UnifiedOrderWhere{ // export class UnifiedOrderWhere{
// [] // []
// } // }
@ -17,6 +18,11 @@ export enum OrderFulfillmentStatus {
// 确认发货 // 确认发货
CONFIRMED, CONFIRMED,
} }
//
export class UnifiedProductWhere {
sku?: string;
[prop:string]:any
}
export class UnifiedTagDTO { export class UnifiedTagDTO {
// 标签DTO用于承载统一标签数据 // 标签DTO用于承载统一标签数据
@ApiProperty({ description: '标签ID' }) @ApiProperty({ description: '标签ID' })
@ -135,8 +141,10 @@ export class UnifiedProductAttributeDTO {
@ApiProperty({ description: '属性选项', type: [String] }) @ApiProperty({ description: '属性选项', type: [String] })
options: string[]; options: string[];
@ApiProperty({ description: '变体属性值(单个值)', required: false }) @ApiProperty({ description: '变体属性值(单个值)', required: false })
option?: string; option?: string;
// 这个是属性的父级字典项
dict?: Dict;
} }
export class UnifiedProductVariationDTO { export class UnifiedProductVariationDTO {

View File

@ -152,7 +152,7 @@ export class QuerySiteDTO {
@Rule(RuleType.boolean().optional()) @Rule(RuleType.boolean().optional())
isDisabled?: boolean; isDisabled?: boolean;
@ApiProperty({ description: '站点ID列表(逗号分隔)', required: false }) @ApiProperty({ description: '站点ID列表(逗号分隔)', required: false })
@Rule(RuleType.string().optional()) @Rule(RuleType.string().optional())
ids?: string; ids?: string;
} }

View File

@ -8,11 +8,11 @@ export interface WooProduct {
id: number; id: number;
// 创建时间 // 创建时间
date_created: string; date_created: string;
// 创建时间GMT // 创建时间(GMT)
date_created_gmt: string; date_created_gmt: string;
// 更新时间 // 更新时间
date_modified: string; date_modified: string;
// 更新时间GMT // 更新时间(GMT)
date_modified_gmt: string; date_modified_gmt: string;
// 产品类型 simple grouped external variable // 产品类型 simple grouped external variable
type: string; type: string;
@ -20,7 +20,7 @@ export interface WooProduct {
status: string; status: string;
// 是否为特色产品 // 是否为特色产品
featured: boolean; featured: boolean;
// 目录可见性选项visible, catalog, search and hidden. Default is visible. // 目录可见性选项:visible, catalog, search and hidden. Default is visible.
catalog_visibility: string; catalog_visibility: string;
// 常规价格 // 常规价格
@ -117,9 +117,9 @@ export interface WooProduct {
// 购买备注 // 购买备注
purchase_note?: string; purchase_note?: string;
// 分类列表 // 分类列表
categories?: Array<{ id: number; name?: string; slug?: string }>; categories?: Array<{ id?: number; name?: string; slug?: string }>;
// 标签列表 // 标签列表
tags?: Array<{ id: number; name?: string; slug?: string }>; tags?: Array<{ id?: number; name?: string; slug?: string }>;
// 菜单排序 // 菜单排序
menu_order?: number; menu_order?: number;
// 元数据 // 元数据
@ -130,11 +130,11 @@ export interface WooVariation {
id: number; id: number;
// 创建时间 // 创建时间
date_created: string; date_created: string;
// 创建时间GMT // 创建时间(GMT)
date_created_gmt: string; date_created_gmt: string;
// 更新时间 // 更新时间
date_modified: string; date_modified: string;
// 更新时间GMT // 更新时间(GMT)
date_modified_gmt: string; date_modified_gmt: string;
// 变体描述 // 变体描述
description: string; description: string;
@ -150,11 +150,11 @@ export interface WooVariation {
price_html?: string; price_html?: string;
// 促销开始日期 // 促销开始日期
date_on_sale_from?: string; date_on_sale_from?: string;
// 促销开始日期GMT // 促销开始日期(GMT)
date_on_sale_from_gmt?: string; date_on_sale_from_gmt?: string;
// 促销结束日期 // 促销结束日期
date_on_sale_to?: string; date_on_sale_to?: string;
// 促销结束日期GMT // 促销结束日期(GMT)
date_on_sale_to_gmt?: string; date_on_sale_to_gmt?: string;
// 是否在促销中 // 是否在促销中
on_sale: boolean; on_sale: boolean;

View File

@ -29,6 +29,10 @@ export class Dict {
@OneToMany(() => DictItem, item => item.dict) @OneToMany(() => DictItem, item => item.dict)
items: DictItem[]; items: DictItem[];
// 排序
@Column({ default: 0, comment: '排序' })
sort: number;
// 是否可删除 // 是否可删除
@Column({ default: true, comment: '是否可删除' }) @Column({ default: true, comment: '是否可删除' })
deletable: boolean; deletable: boolean;

View File

@ -65,9 +65,6 @@ export class Product {
@Column({ type: 'decimal', precision: 10, scale: 2, default: 0 }) @Column({ type: 'decimal', precision: 10, scale: 2, default: 0 })
promotionPrice: number; promotionPrice: number;
// 分类关联 // 分类关联
@ManyToOne(() => Category, category => category.products) @ManyToOne(() => Category, category => category.products)
@JoinColumn({ name: 'categoryId' }) @JoinColumn({ name: 'categoryId' })

View File

@ -42,7 +42,7 @@ export enum OrderStatus {
REFUNDED = 'refunded', // 已退款 REFUNDED = 'refunded', // 已退款
FAILED = 'failed', // 失败订单 FAILED = 'failed', // 失败订单
DRAFT = 'draft', // 草稿 DRAFT = 'draft', // 草稿
AUTO_DRAFT = 'auto-draft', // 自动草稿TODO:不知道为什么出现) AUTO_DRAFT = 'auto-draft', // 自动草稿(TODO:不知道为什么出现)
// TRASH = 'trash', // TRASH = 'trash',
// refund 也就是退款相关的状态 // refund 也就是退款相关的状态

View File

@ -53,7 +53,7 @@ export interface IPlatformService {
getOrder(siteId: number, orderId: string): Promise<any>; getOrder(siteId: number, orderId: string): Promise<any>;
/** /**
* * ()
* @param siteId ID * @param siteId ID
* @returns * @returns
*/ */

View File

@ -20,51 +20,73 @@ import { UnifiedPaginationDTO, UnifiedSearchParamsDTO } from '../dto/api.dto';
import { BatchOperationDTO, BatchOperationResultDTO } from '../dto/batch.dto'; import { BatchOperationDTO, BatchOperationResultDTO } from '../dto/batch.dto';
export interface ISiteAdapter { export interface ISiteAdapter {
mapOrder(order: any): UnifiedOrderDTO; // ========== 客户映射方法 ==========
mapWebhook(webhook:any):UnifiedWebhookDTO;
mapProduct(product:any): UnifiedProductDTO;
mapReview(data: any): UnifiedReviewDTO;
mapCustomer(data: any): UnifiedCustomerDTO;
mapMedia(data: any): UnifiedMediaDTO;
/** /**
* *
* @param data
* @returns
*/ */
getProducts(params: UnifiedSearchParamsDTO): Promise<UnifiedPaginationDTO<UnifiedProductDTO>>; mapPlatformToUnifiedCustomer(data: any): UnifiedCustomerDTO;
/** /**
* *
* @param data
* @returns
*/ */
getAllProducts(params?: UnifiedSearchParamsDTO): Promise<UnifiedProductDTO[]>; mapUnifiedToPlatformCustomer(data: Partial<UnifiedCustomerDTO>): any;
/** /**
* *
*
*/ */
getProduct(id: string | number): Promise<UnifiedProductDTO>; getCustomer(where: Partial<Pick<UnifiedCustomerDTO, 'id' | 'email' | 'phone'>>): Promise<UnifiedCustomerDTO>;
/** /**
* *
*
*/ */
getOrders(params: UnifiedSearchParamsDTO): Promise<UnifiedPaginationDTO<UnifiedOrderDTO>>; getCustomers(params: UnifiedSearchParamsDTO): Promise<UnifiedPaginationDTO<UnifiedCustomerDTO>>;
/** /**
* *
*/ */
getAllOrders(params?: UnifiedSearchParamsDTO): Promise<UnifiedOrderDTO[]>; getAllCustomers(params?: UnifiedSearchParamsDTO): Promise<UnifiedCustomerDTO[]>;
/** /**
* *
*/ */
getOrder(id: string | number): Promise<UnifiedOrderDTO>; createCustomer(data: Partial<UnifiedCustomerDTO>): Promise<UnifiedCustomerDTO>;
/** /**
* *
*/ */
getSubscriptions(params: UnifiedSearchParamsDTO): Promise<UnifiedPaginationDTO<UnifiedSubscriptionDTO>>; updateCustomer(where: Partial<Pick<UnifiedCustomerDTO, 'id' | 'email' | 'phone'>>, data: Partial<UnifiedCustomerDTO>): Promise<UnifiedCustomerDTO>;
/** /**
* *
*/ */
getAllSubscriptions(params?: UnifiedSearchParamsDTO): Promise<UnifiedSubscriptionDTO[]>; deleteCustomer(where: Partial<Pick<UnifiedCustomerDTO, 'id' | 'email' | 'phone'>>): Promise<boolean>;
/**
*
*/
batchProcessCustomers?(data: BatchOperationDTO): Promise<BatchOperationResultDTO>;
// ========== 媒体映射方法 ==========
/**
*
* @param data
* @returns
*/
mapPlatformToUnifiedMedia(data: any): UnifiedMediaDTO;
/**
*
* @param data
* @returns
*/
mapUnifiedToPlatformMedia(data: Partial<UnifiedMediaDTO>): any;
/** /**
* *
@ -81,75 +103,69 @@ export interface ISiteAdapter {
*/ */
createMedia(file: any): Promise<UnifiedMediaDTO>; createMedia(file: any): Promise<UnifiedMediaDTO>;
// ========== 订单映射方法 ==========
/** /**
* *
* @param data
* @returns
*/ */
getReviews(params: UnifiedSearchParamsDTO): Promise<UnifiedPaginationDTO<UnifiedReviewDTO>>; mapPlatformToUnifiedOrder(data: any): UnifiedOrderDTO;
/** /**
* *
* @param data
* @returns
*/ */
getAllReviews(params?: UnifiedSearchParamsDTO): Promise<UnifiedReviewDTO[]>; mapUnifiedToPlatformOrder(data: Partial<UnifiedOrderDTO>): any;
/** /**
* *
* @param data
* @returns
*/ */
createReview(data: CreateReviewDTO): Promise<UnifiedReviewDTO>; mapCreateOrderParams(data: Partial<UnifiedOrderDTO>): any;
/** /**
* *
* @param data
* @returns
*/ */
updateReview(id: number, data: UpdateReviewDTO): Promise<UnifiedReviewDTO>; mapUpdateOrderParams(data: Partial<UnifiedOrderDTO>): any;
/** /**
* *
*/ */
deleteReview(id: number): Promise<boolean>; getOrder(where: Partial<Pick<UnifiedOrderDTO, 'id'>>): Promise<UnifiedOrderDTO>;
/** /**
* *
*/ */
createProduct(data: Partial<UnifiedProductDTO>): Promise<UnifiedProductDTO>; getOrders(params: UnifiedSearchParamsDTO): Promise<UnifiedPaginationDTO<UnifiedOrderDTO>>;
/** /**
* *
*/ */
updateProduct(id: string | number, data: Partial<UnifiedProductDTO>): Promise<boolean>; getAllOrders(params?: UnifiedSearchParamsDTO): Promise<UnifiedOrderDTO[]>;
/** /**
* *
*/ */
deleteProduct(id: string | number): Promise<boolean>; countOrders(params: Record<string, any>): Promise<number>;
/** /**
* *
*/ */
getVariations(productId: string | number, params: UnifiedSearchParamsDTO): Promise<UnifiedVariationPaginationDTO>; createOrder(data: Partial<UnifiedOrderDTO>): Promise<UnifiedOrderDTO>;
/** /**
* *
*/ */
getAllVariations(productId: string | number, params?: UnifiedSearchParamsDTO): Promise<UnifiedProductVariationDTO[]>; updateOrder(where: Partial<Pick<UnifiedOrderDTO, 'id'>>, data: Partial<UnifiedOrderDTO>): Promise<boolean>;
/** /**
* *
*/ */
getVariation(productId: string | number, variationId: string | number): Promise<UnifiedProductVariationDTO>; deleteOrder(where: Partial<Pick<UnifiedOrderDTO, 'id'>>): Promise<boolean>;
/**
*
*/
createVariation(productId: string | number, data: CreateVariationDTO): Promise<UnifiedProductVariationDTO>;
/**
*
*/
updateVariation(productId: string | number, variationId: string | number, data: UpdateVariationDTO): Promise<UnifiedProductVariationDTO>;
/**
*
*/
deleteVariation(productId: string | number, variationId: string | number): Promise<boolean>;
/** /**
* *
@ -161,71 +177,6 @@ export interface ISiteAdapter {
*/ */
createOrderNote(orderId: string | number, data: any): Promise<any>; createOrderNote(orderId: string | number, data: any): Promise<any>;
batchProcessProducts?(data: BatchOperationDTO): Promise<BatchOperationResultDTO>;
createOrder(data: Partial<UnifiedOrderDTO>): Promise<UnifiedOrderDTO>;
updateOrder(id: string | number, data: Partial<UnifiedOrderDTO>): Promise<boolean>;
deleteOrder(id: string | number): Promise<boolean>;
batchProcessOrders?(data: BatchOperationDTO): Promise<BatchOperationResultDTO>;
getCustomers(params: UnifiedSearchParamsDTO): Promise<UnifiedPaginationDTO<UnifiedCustomerDTO>>;
getAllCustomers(params?: UnifiedSearchParamsDTO): Promise<UnifiedCustomerDTO[]>;
getCustomer(id: string | number): Promise<UnifiedCustomerDTO>;
createCustomer(data: Partial<UnifiedCustomerDTO>): Promise<UnifiedCustomerDTO>;
updateCustomer(id: string | number, data: Partial<UnifiedCustomerDTO>): Promise<UnifiedCustomerDTO>;
deleteCustomer(id: string | number): Promise<boolean>;
batchProcessCustomers?(data: BatchOperationDTO): Promise<BatchOperationResultDTO>;
/**
* webhooks列表
*/
getWebhooks(params: UnifiedSearchParamsDTO): Promise<UnifiedWebhookPaginationDTO>;
/**
* webhooks
*/
getAllWebhooks(params?: UnifiedSearchParamsDTO): Promise<UnifiedWebhookDTO[]>;
/**
* webhook
*/
getWebhook(id: string | number): Promise<UnifiedWebhookDTO>;
/**
* webhook
*/
createWebhook(data: CreateWebhookDTO): Promise<UnifiedWebhookDTO>;
/**
* webhook
*/
updateWebhook(id: string | number, data: UpdateWebhookDTO): Promise<UnifiedWebhookDTO>;
/**
* webhook
*/
deleteWebhook(id: string | number): Promise<boolean>;
/**
*
*/
getLinks(): Promise<Array<{title: string, url: string}>>;
/**
*
*/
fulfillOrder(orderId: string | number, data: {
tracking_number?: string;
shipping_provider?: string;
shipping_method?: string;
items?: Array<{
order_item_id: number;
quantity: number;
}>;
}): Promise<any>;
/** /**
* *
*/ */
@ -273,4 +224,276 @@ export interface ISiteAdapter {
* *
*/ */
deleteOrderFulfillment(orderId: string | number, fulfillmentId: string): Promise<boolean>; deleteOrderFulfillment(orderId: string | number, fulfillmentId: string): Promise<boolean>;
/**
*
*/
batchProcessOrders?(data: BatchOperationDTO): Promise<BatchOperationResultDTO>;
// ========== 产品映射方法 ==========
/**
*
* @param data
* @returns
*/
mapPlatformToUnifiedProduct(data: any): UnifiedProductDTO;
/**
*
* @param data
* @returns
*/
mapUnifiedToPlatformProduct(data: Partial<UnifiedProductDTO>): any;
/**
*
* @param data
* @returns
*/
mapCreateProductParams(data: Partial<UnifiedProductDTO>): any;
/**
*
* @param data
* @returns
*/
mapUpdateProductParams(data: Partial<UnifiedProductDTO>): any;
/**
*
*/
getProduct(where: Partial<Pick<UnifiedProductDTO, 'id' | 'sku'>>): Promise<UnifiedProductDTO>;
/**
*
*/
getProducts(params: UnifiedSearchParamsDTO): Promise<UnifiedPaginationDTO<UnifiedProductDTO>>;
/**
*
*/
getAllProducts(params?: UnifiedSearchParamsDTO): Promise<UnifiedProductDTO[]>;
/**
*
*/
createProduct(data: Partial<UnifiedProductDTO>): Promise<UnifiedProductDTO>;
/**
*
*/
updateProduct(where: Partial<Pick<UnifiedProductDTO, 'id' | 'sku'>>, data: Partial<UnifiedProductDTO>): Promise<boolean>;
/**
*
*/
deleteProduct(where: Partial<Pick<UnifiedProductDTO, 'id' | 'sku'>>): Promise<boolean>;
/**
*
*/
batchProcessProducts?(data: BatchOperationDTO): Promise<BatchOperationResultDTO>;
// ========== 评论映射方法 ==========
/**
*
* @param data
* @returns
*/
mapPlatformToUnifiedReview(data: any): UnifiedReviewDTO;
/**
*
* @param data
* @returns
*/
mapUnifiedToPlatformReview(data: Partial<UnifiedReviewDTO>): any;
/**
*
* @param data
* @returns
*/
mapCreateReviewParams(data: CreateReviewDTO): any;
/**
*
* @param data
* @returns
*/
mapUpdateReviewParams(data: UpdateReviewDTO): any;
/**
*
*/
getReviews(params: UnifiedSearchParamsDTO): Promise<UnifiedPaginationDTO<UnifiedReviewDTO>>;
/**
*
*/
getAllReviews(params?: UnifiedSearchParamsDTO): Promise<UnifiedReviewDTO[]>;
/**
*
*/
createReview(data: CreateReviewDTO): Promise<UnifiedReviewDTO>;
/**
*
*/
updateReview(where: Partial<Pick<UnifiedReviewDTO, 'id'>>, data: UpdateReviewDTO): Promise<UnifiedReviewDTO>;
/**
*
*/
deleteReview(where: Partial<Pick<UnifiedReviewDTO, 'id'>>): Promise<boolean>;
// ========== 订阅映射方法 ==========
/**
*
* @param data
* @returns
*/
mapPlatformToUnifiedSubscription(data: any): UnifiedSubscriptionDTO;
/**
*
* @param data
* @returns
*/
mapUnifiedToPlatformSubscription(data: Partial<UnifiedSubscriptionDTO>): any;
/**
*
*/
getSubscriptions(params: UnifiedSearchParamsDTO): Promise<UnifiedPaginationDTO<UnifiedSubscriptionDTO>>;
/**
*
*/
getAllSubscriptions(params?: UnifiedSearchParamsDTO): Promise<UnifiedSubscriptionDTO[]>;
// ========== 产品变体映射方法 ==========
/**
*
* @param data
* @returns
*/
mapPlatformToUnifiedVariation(data: any): UnifiedProductVariationDTO;
/**
*
* @param data
* @returns
*/
mapUnifiedToPlatformVariation(data: Partial<UnifiedProductVariationDTO>): any;
/**
*
* @param data
* @returns
*/
mapCreateVariationParams(data: CreateVariationDTO): any;
/**
*
* @param data
* @returns
*/
mapUpdateVariationParams(data: UpdateVariationDTO): any;
/**
*
*/
getVariation(productId: string | number, variationId: string | number): Promise<UnifiedProductVariationDTO>;
/**
*
*/
getVariations(productId: string | number, params: UnifiedSearchParamsDTO): Promise<UnifiedVariationPaginationDTO>;
/**
*
*/
getAllVariations(productId: string | number, params?: UnifiedSearchParamsDTO): Promise<UnifiedProductVariationDTO[]>;
/**
*
*/
createVariation(productId: string | number, data: CreateVariationDTO): Promise<UnifiedProductVariationDTO>;
/**
*
*/
updateVariation(productId: string | number, variationId: string | number, data: UpdateVariationDTO): Promise<UnifiedProductVariationDTO>;
/**
*
*/
deleteVariation(productId: string | number, variationId: string | number): Promise<boolean>;
// ========== Webhook映射方法 ==========
/**
* Webhook数据转换为统一Webhook数据格式
* @param data Webhook数据
* @returns Webhook数据格式
*/
mapPlatformToUnifiedWebhook(data: any): UnifiedWebhookDTO;
/**
* Webhook数据格式转换为平台Webhook数据
* @param data Webhook数据格式
* @returns Webhook数据
*/
mapUnifiedToPlatformWebhook(data: Partial<UnifiedWebhookDTO>): any;
/**
* Webhook创建参数转换为平台Webhook创建参数
* @param data Webhook创建参数
* @returns Webhook创建参数
*/
mapCreateWebhookParams(data: CreateWebhookDTO): any;
/**
* Webhook更新参数转换为平台Webhook更新参数
* @param data Webhook更新参数
* @returns Webhook更新参数
*/
mapUpdateWebhookParams(data: UpdateWebhookDTO): any;
/**
* webhook
*/
getWebhook(where: Partial<Pick<UnifiedWebhookDTO, 'id'>>): Promise<UnifiedWebhookDTO>;
/**
* webhooks列表
*/
getWebhooks(params: UnifiedSearchParamsDTO): Promise<UnifiedWebhookPaginationDTO>;
/**
* webhooks
*/
getAllWebhooks(params?: UnifiedSearchParamsDTO): Promise<UnifiedWebhookDTO[]>;
/**
* webhook
*/
createWebhook(data: CreateWebhookDTO): Promise<UnifiedWebhookDTO>;
/**
* webhook
*/
updateWebhook(where: Partial<Pick<UnifiedWebhookDTO, 'id'>>, data: UpdateWebhookDTO): Promise<UnifiedWebhookDTO>;
/**
* webhook
*/
deleteWebhook(where: Partial<Pick<UnifiedWebhookDTO, 'id'>>): Promise<boolean>;
// ========== 站点/其他方法 ==========
/**
*
*/
getLinks(): Promise<Array<{ title: string, url: string }>>;
} }

View File

@ -75,10 +75,14 @@ export class SyncUniuniShipmentJob implements IJob{
'255': 'Gateway_To_Gateway_Transit' '255': 'Gateway_To_Gateway_Transit'
}; };
async onTick() { async onTick() {
const shipments:Shipment[] = await this.shipmentModel.findBy({ finished: false }); try {
shipments.forEach(shipment => { const shipments:Shipment[] = await this.shipmentModel.findBy({ finished: false });
this.logisticsService.updateShipmentState(shipment); shipments.forEach(shipment => {
}); this.logisticsService.updateShipmentState(shipment);
});
} catch (error) {
this.logger.error(`更新运单状态失败 ${error.message}`);
}
} }
onComplete(result: any) { onComplete(result: any) {

View File

@ -66,7 +66,7 @@ export class CustomerService {
} }
if (typeof dateValue === 'number') { if (typeof dateValue === 'number') {
// 处理Unix时间戳(秒或毫秒) // 处理Unix时间戳(秒或毫秒)
return new Date(dateValue > 9999999999 ? dateValue : dateValue * 1000); return new Date(dateValue > 9999999999 ? dateValue : dateValue * 1000);
} }
@ -95,7 +95,7 @@ export class CustomerService {
} }
/** /**
* upsert * (upsert)
* *
*/ */
async upsertCustomer( async upsertCustomer(
@ -157,24 +157,24 @@ export class CustomerService {
/** /**
* *
* adapter获取站点客户数据 * 第一步:调用adapter获取站点客户数据
* upsertManyCustomers保存这些客户 * 第二步:通过upsertManyCustomers保存这些客户
*/ */
async syncCustomersFromSite( async syncCustomersFromSite(
siteId: number, siteId: number,
params?: UnifiedSearchParamsDTO params?: UnifiedSearchParamsDTO
): Promise<SyncOperationResult> { ): Promise<SyncOperationResult> {
try { try {
// 第一步获取适配器并从站点获取客户数据 // 第一步:获取适配器并从站点获取客户数据
const adapter = await this.siteApiService.getAdapter(siteId); const adapter = await this.siteApiService.getAdapter(siteId);
const siteCustomers = await adapter.getAllCustomers(params || {}); const siteCustomers = await adapter.getAllCustomers(params || {});
// 第二步将站点客户数据转换为客户实体数据 // 第二步:将站点客户数据转换为客户实体数据
const customersData = siteCustomers.map(siteCustomer => { const customersData = siteCustomers.map(siteCustomer => {
return this.mapSiteCustomerToCustomer(siteCustomer, siteId); return this.mapSiteCustomerToCustomer(siteCustomer, siteId);
}) })
// 第三步批量upsert客户数据 // 第三步:批量upsert客户数据
const upsertResult = await this.upsertManyCustomers(customersData); const upsertResult = await this.upsertManyCustomers(customersData);
return { return {
total: siteCustomers.length, total: siteCustomers.length,
@ -192,7 +192,7 @@ export class CustomerService {
} }
/** /**
* * ()
* *
* 使SQL查询实现复杂的统计逻辑 * 使SQL查询实现复杂的统计逻辑
*/ */
@ -363,7 +363,7 @@ export class CustomerService {
} }
/** /**
* * ()
* *
* 使TypeORM查询构建器实现 * 使TypeORM查询构建器实现
*/ */

View File

@ -239,7 +239,7 @@ export class DictService {
} }
// 更新或创建字典项 (Upsert) // 更新或创建字典项 (Upsert)
// 如果字典项已存在(根据 name 和 dictId 判断),则更新;否则创建新的 // 如果字典项已存在(根据 name 和 dictId 判断),则更新;否则创建新的
async upsertDictItem(dictId: number, itemData: { async upsertDictItem(dictId: number, itemData: {
name: string; name: string;
title: string; title: string;
@ -252,7 +252,7 @@ export class DictService {
// 格式化 name // 格式化 name
const formattedName = this.formatName(itemData.name); const formattedName = this.formatName(itemData.name);
// 查找是否已存在该字典项(根据 name 和 dictId // 查找是否已存在该字典项(根据 name 和 dictId)
const existingItem = await this.dictItemModel.findOne({ const existingItem = await this.dictItemModel.findOne({
where: { where: {
name: formattedName, name: formattedName,

View File

@ -202,7 +202,7 @@ export class OrderService {
try { try {
// 调用 WooCommerce API 获取订单 // 调用 WooCommerce API 获取订单
const adapter = await this.siteApiService.getAdapter(siteId); const adapter = await this.siteApiService.getAdapter(siteId);
const order = await adapter.getOrder(orderId); const order = await adapter.getOrder({ id: orderId });
// 检查订单是否已存在,以区分创建和更新 // 检查订单是否已存在,以区分创建和更新
const existingOrder = await this.orderModel.findOne({ const existingOrder = await this.orderModel.findOne({
@ -479,7 +479,7 @@ export class OrderService {
// 如果不能更新 ERP 状态,则保留原有的 orderStatus // 如果不能更新 ERP 状态,则保留原有的 orderStatus
entity.orderStatus = existingOrder.orderStatus; entity.orderStatus = existingOrder.orderStatus;
} }
// 更新订单数据(包括 shipping、billing 等字段) // 更新订单数据(包括 shipping、billing 等字段)
await this.orderModel.update(existingOrder.id, entity); await this.orderModel.update(existingOrder.id, entity);
entity.id = existingOrder.id; entity.id = existingOrder.id;
return entity; return entity;
@ -2567,8 +2567,8 @@ export class OrderService {
* CSV格式 * CSV格式
* @param {any[]} data * @param {any[]} data
* @param {Object} options * @param {Object} options
* @param {string} [options.type='string'] 'string' | 'buffer' * @param {string} [options.type='string'] :'string' | 'buffer'
* @param {string} [options.fileName] 使 * @param {string} [options.fileName] (使)
* @param {boolean} [options.writeFile=false] * @param {boolean} [options.writeFile=false]
* @returns {string|Buffer} type返回字符串或Buffer * @returns {string|Buffer} type返回字符串或Buffer
*/ */
@ -2617,7 +2617,7 @@ async exportToCsv(data: any[], options: { type?: 'string' | 'buffer'; fileName?:
// 获取当前用户目录 // 获取当前用户目录
const userHomeDir = os.homedir(); const userHomeDir = os.homedir();
// 构建目标路径(下载目录) // 构建目标路径(下载目录)
const downloadsDir = path.join(userHomeDir, 'Downloads'); const downloadsDir = path.join(userHomeDir, 'Downloads');
// 确保下载目录存在 // 确保下载目录存在

View File

@ -28,7 +28,7 @@ import { StockPoint } from '../entity/stock_point.entity';
import { StockService } from './stock.service'; import { StockService } from './stock.service';
import { TemplateService } from './template.service'; import { TemplateService } from './template.service';
import { SyncOperationResultDTO, UnifiedSearchParamsDTO } from '../dto/api.dto'; import { BatchErrorItem, BatchOperationResult, SyncOperationResultDTO, UnifiedSearchParamsDTO } from '../dto/api.dto';
import { UnifiedProductDTO } from '../dto/site-api.dto'; import { UnifiedProductDTO } from '../dto/site-api.dto';
import { ProductSiteSkuDTO, SyncProductToSiteDTO } from '../dto/site-sync.dto'; import { ProductSiteSkuDTO, SyncProductToSiteDTO } from '../dto/site-sync.dto';
import { Category } from '../entity/category.entity'; import { Category } from '../entity/category.entity';
@ -225,7 +225,7 @@ export class ProductService {
where: { where: {
sku, sku,
}, },
relations: ['category', 'attributes', 'attributes.dict', 'siteSkus'] relations: ['category', 'attributes', 'attributes.dict']
}); });
} }
@ -235,7 +235,7 @@ export class ProductService {
.leftJoinAndSelect('product.attributes', 'attribute') .leftJoinAndSelect('product.attributes', 'attribute')
.leftJoinAndSelect('attribute.dict', 'dict') .leftJoinAndSelect('attribute.dict', 'dict')
.leftJoinAndSelect('product.category', 'category'); .leftJoinAndSelect('product.category', 'category');
// 处理分页参数(支持新旧两种格式) // 处理分页参数(支持新旧两种格式)
const page = query.page || 1; const page = query.page || 1;
const pageSize = query.per_page || 10; const pageSize = query.per_page || 10;
@ -393,7 +393,7 @@ export class ProductService {
qb.andWhere('product.updatedAt <= :whereUpdatedAtEnd', { whereUpdatedAtEnd: new Date(query.where.updatedAtEnd) }); qb.andWhere('product.updatedAt <= :whereUpdatedAtEnd', { whereUpdatedAtEnd: new Date(query.where.updatedAtEnd) });
} }
// 品牌过滤(向后兼容) // 品牌过滤(向后兼容)
if (brandId) { if (brandId) {
qb.andWhere(qb => { qb.andWhere(qb => {
const subQuery = qb const subQuery = qb
@ -423,7 +423,7 @@ export class ProductService {
}); });
} }
// 分类过滤(向后兼容) // 分类过滤(向后兼容)
if (categoryId) { if (categoryId) {
qb.andWhere('product.categoryId = :categoryId', { categoryId }); qb.andWhere('product.categoryId = :categoryId', { categoryId });
} }
@ -443,7 +443,7 @@ export class ProductService {
qb.andWhere('product.categoryId IN (:...whereCategoryIds)', { whereCategoryIds: query.where.categoryIds }); qb.andWhere('product.categoryId IN (:...whereCategoryIds)', { whereCategoryIds: query.where.categoryIds });
} }
// 处理排序(支持新旧两种格式) // 处理排序(支持新旧两种格式)
if (orderBy) { if (orderBy) {
if (typeof orderBy === 'string') { if (typeof orderBy === 'string') {
// 如果orderBy是字符串尝试解析JSON // 如果orderBy是字符串尝试解析JSON
@ -1455,6 +1455,9 @@ export class ProductService {
} }
} }
// 处理分类字段
const category = val(rec.category);
return { return {
sku, sku,
name: val(rec.name), name: val(rec.name),
@ -1464,6 +1467,7 @@ export class ProductService {
promotionPrice: num(rec.promotionPrice), promotionPrice: num(rec.promotionPrice),
type: val(rec.type), type: val(rec.type),
siteSkus: rec.siteSkus ? String(rec.siteSkus).split(',').map(s => s.trim()).filter(Boolean) : undefined, siteSkus: rec.siteSkus ? String(rec.siteSkus).split(',').map(s => s.trim()).filter(Boolean) : undefined,
category, // 添加分类字段
attributes: attributes.length > 0 ? attributes : undefined, attributes: attributes.length > 0 ? attributes : undefined,
} as any; } as any;
@ -1483,10 +1487,15 @@ export class ProductService {
if (data.price !== undefined) dto.price = Number(data.price); if (data.price !== undefined) dto.price = Number(data.price);
if (data.promotionPrice !== undefined) dto.promotionPrice = Number(data.promotionPrice); if (data.promotionPrice !== undefined) dto.promotionPrice = Number(data.promotionPrice);
if (data.categoryId !== undefined) dto.categoryId = Number(data.categoryId); // 处理分类字段
if (data.categoryId !== undefined) {
dto.categoryId = Number(data.categoryId);
} else if (data.category) {
// 如果是字符串需要后续在createProduct中处理
dto.attributes = [...(dto.attributes || []), { dictName: 'category', title: data.category }];
}
// 默认值和特殊处理 // 默认值和特殊处理
dto.attributes = Array.isArray(data.attributes) ? data.attributes : []; dto.attributes = Array.isArray(data.attributes) ? data.attributes : [];
// 如果有组件信息,透传 // 如果有组件信息,透传
@ -1508,7 +1517,13 @@ export class ProductService {
if (data.price !== undefined) dto.price = Number(data.price); if (data.price !== undefined) dto.price = Number(data.price);
if (data.promotionPrice !== undefined) dto.promotionPrice = Number(data.promotionPrice); if (data.promotionPrice !== undefined) dto.promotionPrice = Number(data.promotionPrice);
if (data.categoryId !== undefined) dto.categoryId = Number(data.categoryId); // 处理分类字段
if (data.categoryId !== undefined) {
dto.categoryId = Number(data.categoryId);
} else if (data.category) {
// 如果是字符串需要后续在updateProduct中处理
dto.attributes = [...(dto.attributes || []), { dictName: 'category', title: data.category }];
}
if (data.type !== undefined) dto.type = data.type; if (data.type !== undefined) dto.type = data.type;
if (data.attributes !== undefined) dto.attributes = data.attributes; if (data.attributes !== undefined) dto.attributes = data.attributes;
@ -1548,8 +1563,8 @@ export class ProductService {
esc(p.price), esc(p.price),
esc(p.promotionPrice), esc(p.promotionPrice),
esc(p.type), esc(p.type),
esc(p.description), esc(p.description),
esc(p.category ? p.category.name || p.category.title : ''), // 添加分类字段
]; ];
// 属性数据 // 属性数据
@ -1575,9 +1590,9 @@ export class ProductService {
// 导出所有产品为 CSV 文本 // 导出所有产品为 CSV 文本
async exportProductsCSV(): Promise<string> { async exportProductsCSV(): Promise<string> {
// 查询所有产品及其属性(包含字典关系)和组成 // 查询所有产品及其属性(包含字典关系)、组成和分类
const products = await this.productModel.find({ const products = await this.productModel.find({
relations: ['attributes', 'attributes.dict', 'components'], relations: ['attributes', 'attributes.dict', 'components', 'category'],
order: { id: 'ASC' }, order: { id: 'ASC' },
}); });
@ -1612,8 +1627,8 @@ export class ProductService {
'price', 'price',
'promotionPrice', 'promotionPrice',
'type', 'type',
'description', 'description',
'category',
]; ];
// 动态属性表头 // 动态属性表头
@ -1640,7 +1655,7 @@ export class ProductService {
} }
// 从 CSV 导入产品;存在则更新,不存在则创建 // 从 CSV 导入产品;存在则更新,不存在则创建
async importProductsCSV(file: any): Promise<{ created: number; updated: number; errors: string[] }> { async importProductsCSV(file: any): Promise<BatchOperationResult> {
let buffer: Buffer; let buffer: Buffer;
if (Buffer.isBuffer(file)) { if (Buffer.isBuffer(file)) {
buffer = file; buffer = file;
@ -1676,19 +1691,19 @@ export class ProductService {
console.log('First record keys:', Object.keys(records[0])); console.log('First record keys:', Object.keys(records[0]));
} }
} catch (e: any) { } catch (e: any) {
return { created: 0, updated: 0, errors: [`CSV 解析失败:${e?.message || e}`] }; throw new Error(`CSV 解析失败:${e?.message || e}`)
} }
let created = 0; let created = 0;
let updated = 0; let updated = 0;
const errors: string[] = []; const errors: BatchErrorItem[] = [];
// 逐条处理记录 // 逐条处理记录
for (const rec of records) { for (const rec of records) {
try { try {
const data = this.transformCsvRecordToData(rec); const data = this.transformCsvRecordToData(rec);
if (!data) { if (!data) {
errors.push('缺少 SKU 的记录已跳过'); errors.push({ identifier: data.sku, error: '缺少 SKU 的记录已跳过'});
continue; continue;
} }
const { sku } = data; const { sku } = data;
@ -1708,11 +1723,11 @@ export class ProductService {
updated += 1; updated += 1;
} }
} catch (e: any) { } catch (e: any) {
errors.push(`产品${rec?.sku}导入失败:${e?.message || String(e)}`); errors.push({ identifier: '' + rec.sku, error: `产品${rec?.sku}导入失败:${e?.message || String(e)}`});
} }
} }
return { created, updated, errors }; return { total: records.length, processed: records.length - errors.length, created, updated, errors };
} }
// 将库存记录的 sku 添加到产品单品中 // 将库存记录的 sku 添加到产品单品中
@ -1750,7 +1765,7 @@ export class ProductService {
} }
// 根据ID获取产品详情包含站点SKU // 根据ID获取产品详情(包含站点SKU)
async getProductById(id: number): Promise<Product> { async getProductById(id: number): Promise<Product> {
const product = await this.productModel.findOne({ const product = await this.productModel.findOne({
where: { id }, where: { id },
@ -1831,9 +1846,7 @@ export class ProductService {
} }
// 将本地产品转换为站点API所需格式 // 将本地产品转换为站点API所需格式
const unifiedProduct = await this.convertLocalProductToUnifiedProduct(localProduct, params.siteSku); const unifiedProduct = await this.mapLocalToUnifiedProduct(localProduct, params.siteSku);
// 调用站点API的upsertProduct方法 // 调用站点API的upsertProduct方法
try { try {
@ -1842,7 +1855,7 @@ export class ProductService {
await this.bindSiteSkus(localProduct.id, [unifiedProduct.sku]); await this.bindSiteSkus(localProduct.id, [unifiedProduct.sku]);
return result; return result;
} catch (error) { } catch (error) {
throw new Error(`同步产品到站点失败: ${error.message}`); throw new Error(`同步产品到站点失败: ${error?.response?.data?.message??error.message}`);
} }
} }
@ -1869,9 +1882,6 @@ export class ProductService {
siteSku: item.siteSku siteSku: item.siteSku
}); });
// 然后绑定站点SKU
await this.bindSiteSkus(item.productId, [item.siteSku]);
results.synced++; results.synced++;
results.processed++; results.processed++;
} catch (error) { } catch (error) {
@ -1892,30 +1902,23 @@ export class ProductService {
* @param siteProductId ID * @param siteProductId ID
* @returns * @returns
*/ */
async syncProductFromSite(siteId: number, siteProductId: string | number): Promise<any> { async syncProductFromSite(siteId: number, siteProductId: string | number, sku: string): Promise<any> {
const adapter = await this.siteApiService.getAdapter(siteId);
const siteProduct = await adapter.getProduct({ id: siteProductId });
// 从站点获取产品信息 // 从站点获取产品信息
const siteProduct = await this.siteApiService.getProductFromSite(siteId, siteProductId);
if (!siteProduct) { if (!siteProduct) {
throw new Error(`站点产品 ID ${siteProductId} 不存在`); throw new Error(`站点产品 ID ${siteProductId} 不存在`);
} }
// 检查是否已存在相同SKU的本地产品
let localProduct = null;
if (siteProduct.sku) {
try {
localProduct = await this.findProductBySku(siteProduct.sku);
} catch (error) {
// 产品不存在,继续创建
}
}
// 将站点产品转换为本地产品格式 // 将站点产品转换为本地产品格式
const productData = await this.convertSiteProductToLocalProduct(siteProduct); const productData = await this.mapUnifiedToLocalProduct(siteProduct);
return await this.upsertProduct({sku}, productData);
if (localProduct) { }
async upsertProduct(where: Partial<Pick<Product,'id'| 'sku'>>, productData: any) {
const existingProduct = await this.productModel.findOne({ where: where});
if (existingProduct) {
// 更新现有产品 // 更新现有产品
const updateData: UpdateProductDTO = productData; const updateData: UpdateProductDTO = productData;
return await this.updateProduct(localProduct.id, updateData); return await this.updateProduct(existingProduct.id, updateData);
} else { } else {
// 创建新产品 // 创建新产品
const createData: CreateProductDTO = productData; const createData: CreateProductDTO = productData;
@ -1929,18 +1932,18 @@ export class ProductService {
* @param siteProductIds ID数组 * @param siteProductIds ID数组
* @returns * @returns
*/ */
async batchSyncFromSite(siteId: number, siteProductIds: (string | number)[]): Promise<{ synced: number, errors: string[] }> { async batchSyncFromSite(siteId: number, data: Array<{siteProductId:string, sku: string}>): Promise<{ synced: number, errors: string[] }> {
const results = { const results = {
synced: 0, synced: 0,
errors: [] errors: []
}; };
for (const siteProductId of siteProductIds) { for (const item of data) {
try { try {
await this.syncProductFromSite(siteId, siteProductId); await this.syncProductFromSite(siteId, item.siteProductId, item.sku);
results.synced++; results.synced++;
} catch (error) { } catch (error) {
results.errors.push(`站点产品ID ${siteProductId} 同步失败: ${error.message}`); results.errors.push(`站点产品ID ${item.siteProductId} 同步失败: ${error.message}`);
} }
} }
@ -1952,7 +1955,7 @@ export class ProductService {
* @param siteProduct * @param siteProduct
* @returns * @returns
*/ */
private async convertSiteProductToLocalProduct(siteProduct: any): Promise<CreateProductDTO> { private async mapUnifiedToLocalProduct(siteProduct: any): Promise<CreateProductDTO> {
const productData: any = { const productData: any = {
sku: siteProduct.sku, sku: siteProduct.sku,
name: siteProduct.name, name: siteProduct.name,
@ -2015,18 +2018,20 @@ export class ProductService {
* @param localProduct * @param localProduct
* @returns * @returns
*/ */
private async convertLocalProductToUnifiedProduct(localProduct: Product,siteSku?: string): Promise<Partial<UnifiedProductDTO>> { private async mapLocalToUnifiedProduct(localProduct: Product,siteSku?: string): Promise<Partial<UnifiedProductDTO>> {
const tags = localProduct.attributes?.map(a => ({name: a.name})) || [];
// 将本地产品数据转换为UnifiedProductDTO格式 // 将本地产品数据转换为UnifiedProductDTO格式
const unifiedProduct: any = { const unifiedProduct: any = {
id: localProduct.id ? String(localProduct.id) : undefined, // 如果产品已存在使用现有ID id: localProduct.id ? String(localProduct.id) : undefined, // 如果产品已存在使用现有ID
name: localProduct.nameCn || localProduct.name || localProduct.sku, name: localProduct.name,
type: 'simple', // 默认类型,可以根据实际需要调整 type: localProduct.type === 'single'? 'simple' : 'bundle', // 默认类型,可以根据实际需要调整
status: 'publish', // 默认状态,可以根据实际需要调整 status: 'publish', // 默认状态,可以根据实际需要调整
sku: siteSku || await this.templateService.render('site.product.sku', { sku: localProduct.sku }), sku: siteSku || await this.templateService.render('site.product.sku', { product: localProduct, sku: localProduct.sku }),
regular_price: String(localProduct.price || 0), regular_price: String(localProduct.price || 0),
sale_price: String(localProduct.promotionPrice || localProduct.price || 0), sale_price: String(localProduct.promotionPrice || localProduct.price || 0),
price: String(localProduct.price || 0), price: String(localProduct.price || 0),
// stock_status: localProduct.stockQuantity && localProduct.stockQuantity > 0 ? 'instock' : 'outofstock', // TODO 库存暂时无法同步
// stock_status: localProduct.components && localProduct.stockQuantity > 0 ? 'instock' : 'outofstock',
// stock_quantity: localProduct.stockQuantity || 0, // stock_quantity: localProduct.stockQuantity || 0,
// images: localProduct.images ? localProduct.images.map(img => ({ // images: localProduct.images ? localProduct.images.map(img => ({
// id: img.id, // id: img.id,
@ -2034,25 +2039,24 @@ export class ProductService {
// name: img.name || '', // name: img.name || '',
// alt: img.alt || '' // alt: img.alt || ''
// })) : [], // })) : [],
tags: [], tags,
categories: localProduct.category ? [{ categories: localProduct.category ? [{
id: localProduct.category.id, id: localProduct.category.id,
name: localProduct.category.name name: localProduct.category.name
}] : [], }] : [],
attributes: localProduct.attributes ? localProduct.attributes.map(attr => ({ attributes: localProduct.attributes ? localProduct.attributes.map(attr => ({
id: attr.id, id: attr.dict.id,
name: attr.name, name: attr.dict.name,
position: 0, position: attr.dict.sort || 0,
visible: true, visible: true,
variation: false, variation: false,
options: [attr.value] options: [attr.name]
})) : [], })) : [],
variations: [], variations: [],
date_created: localProduct.createdAt ? new Date(localProduct.createdAt).toISOString() : new Date().toISOString(), date_created: localProduct.createdAt ? new Date(localProduct.createdAt).toISOString() : new Date().toISOString(),
date_modified: localProduct.updatedAt ? new Date(localProduct.updatedAt).toISOString() : new Date().toISOString(), date_modified: localProduct.updatedAt ? new Date(localProduct.updatedAt).toISOString() : new Date().toISOString(),
raw: { raw: {
localProductId: localProduct.id, ...localProduct
localProductSku: localProduct.sku
} }
}; };

View File

@ -1,3 +1,6 @@
/**
* https://www.apizza.net/project/e114fb8e628e0f604379f5b26f0d8330/browse
*/
import { ILogger, Inject, Provide } from '@midwayjs/core'; import { ILogger, Inject, Provide } from '@midwayjs/core';
import axios, { AxiosRequestConfig } from 'axios'; import axios, { AxiosRequestConfig } from 'axios';
import * as fs from 'fs'; import * as fs from 'fs';
@ -125,7 +128,7 @@ export class ShopyyService {
* @returns URL * @returns URL
*/ */
private buildURL(baseUrl: string, endpoint: string): string { 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 base = baseUrl.replace(/\/$/, '');
const end = endpoint.replace(/^\//, ''); const end = endpoint.replace(/^\//, '');
return `${base}/${end}`; return `${base}/${end}`;
@ -155,7 +158,7 @@ export class ShopyyService {
* @param params * @param params
* @returns * @returns
*/ */
private async request(site: any, endpoint: string, method: string = 'GET', data: any = null, params: any = null): Promise<any> { async request(site: any, endpoint: string, method: string = 'GET', data: any = null, params: any = null): Promise<any> {
const url = this.buildURL(site.apiUrl, endpoint); const url = this.buildURL(site.apiUrl, endpoint);
const headers = this.buildHeaders(site); const headers = this.buildHeaders(site);
@ -180,41 +183,19 @@ export class ShopyyService {
* *
*/ */
public async fetchResourcePaged<T>(site: any, endpoint: string, params: Record<string, any> = {}) { public async fetchResourcePaged<T>(site: any, endpoint: string, params: Record<string, any> = {}) {
const page = Number(params.page || 1); const response = await this.request(site, endpoint, 'GET', null, params);
const limit = Number(params.per_page ?? 20); return this.mapPageResponse<T>(response,params);
const where = params.where && typeof params.where === 'object' ? params.where : {}; }
let orderby: string | undefined = params.orderby; mapPageResponse<T>(response:any,query: Record<string, any>){
let order: 'asc' | 'desc' | undefined = params.orderDir as any;
if (!orderby && params.order && typeof params.order === 'object') {
const entries = Object.entries(params.order as Record<string, any>);
if (entries.length > 0) {
const [field, dir] = entries[0];
orderby = field;
order = String(dir).toLowerCase() === 'desc' ? 'desc' : 'asc';
}
}
// 映射统一入参到平台入参
const requestParams = {
...where,
...(params.search ? { search: params.search } : {}),
...(params.status ? { status: params.status } : {}),
...(orderby ? { orderby } : {}),
...(order ? { order } : {}),
page,
limit
};
this.logger.debug('ShopYY API请求分页参数:'+ JSON.stringify(requestParams));
const response = await this.request(site, endpoint, 'GET', null, requestParams);
if (response?.code !== 0) { if (response?.code !== 0) {
throw new Error(response?.msg) throw new Error(response?.msg)
} }
return { return {
items: (response.data.list || []) as T[], items: (response.data.list || []) as T[],
total: response.data?.paginate?.total || 0, total: response.data?.paginate?.total || 0,
totalPages: response.data?.paginate?.pageTotal || 0, totalPages: response.data?.paginate?.pageTotal || 0,
page: response.data?.paginate?.current || requestParams.page, page: response.data?.paginate?.current || query.page,
per_page: response.data?.paginate?.pagesize || requestParams.limit, per_page: response.data?.paginate?.pagesize || query.limit,
}; };
} }
@ -225,13 +206,13 @@ export class ShopyyService {
* @param pageSize * @param pageSize
* @returns * @returns
*/ */
async getProducts(site: any, page: number = 1, pageSize: number = 100): Promise<any> { async getProducts(site: any, page: number = 1, pageSize: number = 100, where: Record<string, any> = {}): Promise<any> {
// ShopYY API: GET /products // ShopYY API: GET /products
// 通过 fields 参数指定需要返回的字段,确保 handle 等关键信息被包含 // 通过 fields 参数指定需要返回的字段,确保 handle 等关键信息被包含
const response = await this.request(site, 'products', 'GET', null, { const response = await this.request(site, 'products', 'GET', null, {
page, page,
page_size: pageSize, page_size: pageSize,
fields: 'id,name,sku,handle,status,type,stock_status,stock_quantity,images,regular_price,sale_price,tags,variations' ...where
}); });
return { return {

View File

@ -1,4 +1,4 @@
import { Inject, Provide } from '@midwayjs/core'; import { ILogger, Inject, Provide } from '@midwayjs/core';
import { ShopyyAdapter } from '../adapter/shopyy.adapter'; import { ShopyyAdapter } from '../adapter/shopyy.adapter';
import { WooCommerceAdapter } from '../adapter/woocommerce.adapter'; import { WooCommerceAdapter } from '../adapter/woocommerce.adapter';
import { ISiteAdapter } from '../interface/site-adapter.interface'; import { ISiteAdapter } from '../interface/site-adapter.interface';
@ -22,6 +22,9 @@ export class SiteApiService {
@Inject() @Inject()
productService: ProductService; productService: ProductService;
@Inject()
logger: ILogger;
async getAdapter(siteId: number): Promise<ISiteAdapter> { async getAdapter(siteId: number): Promise<ISiteAdapter> {
const site = await this.siteService.get(siteId, true); const site = await this.siteService.get(siteId, true);
if (!site) { if (!site) {
@ -110,36 +113,25 @@ export class SiteApiService {
const adapter = await this.getAdapter(siteId); const adapter = await this.getAdapter(siteId);
// 首先尝试查找产品 // 首先尝试查找产品
if (product.id) { if (!product.sku) {
try { throw new Error('产品SKU不能为空');
// 尝试获取产品以确认它是否存在
const existingProduct = await adapter.getProduct(product.id);
if (existingProduct) {
// 产品存在,执行更新
return await adapter.updateProduct(product.id, product);
}
} catch (error) {
// 如果获取产品失败,可能是因为产品不存在,继续执行创建逻辑
console.log(`产品 ${product.id} 不存在,将创建新产品:`, error.message);
}
} else if (product.sku) {
// 如果没有提供ID但提供了SKU尝试通过SKU查找产品
try {
// 尝试搜索具有相同SKU的产品
const searchResult = await adapter.getProducts({ where: { sku: product.sku } });
if (searchResult.items && searchResult.items.length > 0) {
const existingProduct = searchResult.items[0];
// 找到现有产品,更新它
return await adapter.updateProduct(existingProduct.id, product);
}
} catch (error) {
// 搜索失败,继续执行创建逻辑
console.log(`通过SKU搜索产品失败:`, error.message);
}
} }
// 尝试搜索具有相同SKU的产品
let existingProduct
try {
existingProduct = await adapter.getProduct({ sku: product.sku });
} catch (error) {
this.logger.error(`[Site API] 查找产品失败, siteId: ${siteId}, sku: ${product.sku}, 错误信息: ${error.message}`);
existingProduct = null
}
if (existingProduct) {
// 找到现有产品,更新它
return await adapter.updateProduct({ id: existingProduct.id }, product);
}
// 产品不存在,执行创建 // 产品不存在,执行创建
return await adapter.createProduct(product); return await adapter.createProduct(product);
} }
/** /**
@ -160,7 +152,7 @@ export class SiteApiService {
const result = await this.upsertProduct(siteId, product); const result = await this.upsertProduct(siteId, product);
// 判断是创建还是更新 // 判断是创建还是更新
if (result && result.id) { if (result && result.id) {
// 简单判断如果产品原本没有ID而现在有了说明是创建的 // 简单判断:如果产品原本没有ID而现在有了说明是创建的
if (!product.id || !product.id.toString().trim()) { if (!product.id || !product.id.toString().trim()) {
results.created.push(result); results.created.push(result);
} else { } else {
@ -189,17 +181,6 @@ export class SiteApiService {
return await adapter.getProducts(params); return await adapter.getProducts(params);
} }
/**
*
* @param siteId ID
* @param productId ID
* @returns
*/
async getProductFromSite(siteId: number, productId: string | number): Promise<any> {
const adapter = await this.getAdapter(siteId);
return await adapter.getProduct(productId);
}
/** /**
* *
* @param siteId ID * @param siteId ID

View File

@ -44,7 +44,7 @@ export class WPService implements IPlatformService {
* @param site * @param site
* @param namespace API , wc/v3; wcs/v1 * @param namespace API , wc/v3; wcs/v1
*/ */
private createApi(site: any, namespace: WooCommerceRestApiVersion = 'wc/v3') { public createApi(site: any, namespace: WooCommerceRestApiVersion = 'wc/v3') {
return new WooCommerceRestApi({ return new WooCommerceRestApi({
url: site.apiUrl, url: site.apiUrl,
consumerKey: site.consumerKey, consumerKey: site.consumerKey,
@ -240,9 +240,11 @@ export class WPService implements IPlatformService {
return allData; return allData;
} }
async getProducts(site: any, page: number = 1, pageSize: number = 100): Promise<any> { async getProducts(site: any, params: Record<string, any> = {}): Promise<any> {
const api = this.createApi(site, 'wc/v3'); const api = this.createApi(site, 'wc/v3');
return await this.sdkGetPage<WooProduct>(api, 'products', { page, per_page: pageSize }); const page = params.page ?? 1;
const per_page = params.per_page ?? params.pageSize ?? 100;
return await this.sdkGetPage<WooProduct>(api, 'products', { ...params, page, per_page });
} }
async getProduct(site: any, id: number): Promise<any> { async getProduct(site: any, id: number): Promise<any> {
@ -252,9 +254,9 @@ export class WPService implements IPlatformService {
} }
// 导出 WooCommerce 产品为特殊CSV(平台特性) // 导出 WooCommerce 产品为特殊CSV(平台特性)
async exportProductsCsvSpecial(site: any, page: number = 1, pageSize: number = 100): Promise<string> { async exportProductsCsvSpecial(site: any, page: number = 1, pageSize: number = 100): Promise<string> {
const list = await this.getProducts(site, page, pageSize); 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']; const header = ['id','name','type','status','sku','regular_price','sale_price','stock_status','stock_quantity'];
const rows = (list.items || []).map((p: any) => [p.id,p.name,p.type,p.status,p.sku,p.regular_price,p.sale_price,p.stock_status,p.stock_quantity]); const rows = (list.items || []).map((p: any) => [p.id,p.name,p.type,p.status,p.sku,p.regular_price,p.sale_price,p.stock_status,p.stock_quantity]);
const csv = [header.join(','), ...rows.map(r => r.map(v => String(v ?? '')).join(','))].join('\n'); const csv = [header.join(','), ...rows.map(r => r.map(v => String(v ?? '')).join(','))].join('\n');

View File

@ -0,0 +1 @@
// 从 unified 到 数据库需要有个转换流程

View File

@ -0,0 +1 @@
// 文件转换

View File

View File

@ -0,0 +1,8 @@
import { UnifiedOrderDTO } from "../dto/site-api.dto";
export class ShipmentAdapter {
// 用于导出物流需要的数据
mapFromOrder(order: UnifiedOrderDTO): any {
return order;
}
}

View File

@ -1,105 +0,0 @@
# 产品站点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响应