Compare commits
54 Commits
main-backu
...
main
| Author | SHA1 | Date |
|---|---|---|
|
|
3968fd8965 | |
|
|
c26918d4db | |
|
|
16d27179e7 | |
|
|
a556ab69bf | |
|
|
efbd318917 | |
|
|
e8424afd91 | |
|
|
5f16801f98 | |
|
|
22a13ce0b8 | |
|
|
2f57dc0d8c | |
|
|
926e531d4f | |
|
|
66a70f6209 | |
|
|
1eacee307d | |
|
|
bff03de8b0 | |
|
|
86aa5f5790 | |
|
|
52fa7d651e | |
|
|
0ea834218d | |
|
|
86fd31ac12 | |
|
|
75056db42c | |
|
|
d5384944a4 | |
|
|
cb876e8c0f | |
|
|
71b2c249be | |
|
|
b3b7ee4793 | |
|
|
b7101ac866 | |
|
|
72cd20fcd6 | |
|
|
8766cf4a4c | |
|
|
d39341d683 | |
|
|
7f04de4583 | |
|
|
bdac4860df | |
|
|
fff62d6864 | |
|
|
c75c0a614f | |
|
|
bfa03fc6a0 | |
|
|
16539b133f | |
|
|
9fc1bedb0c | |
|
|
0f79b7536a | |
|
|
fbbb86ae37 | |
|
|
56deb447b3 | |
|
|
68574dbc7a | |
|
|
eb5cb215a9 | |
|
|
ca0b5e63a7 | |
|
|
5d7e0090aa | |
|
|
ecdedcc041 | |
|
|
b2ee61e47d | |
|
|
64c1d1afe5 | |
|
|
4eb45af452 | |
|
|
a8d12a695e | |
|
|
a00a95c9a3 | |
|
|
82c8640f0c | |
|
|
cb00076bd3 | |
|
|
cdff083940 | |
|
|
3d6f488a70 | |
|
|
a067720c76 | |
|
|
2888d62037 | |
|
|
3f5fb6adba | |
|
|
983ba47dbf |
|
|
@ -1,49 +0,0 @@
|
|||
# 数据库迁移指南
|
||||
|
||||
为了支持区域坐标功能,需要执行数据库迁移操作,将新增的 `latitude` 和 `longitude` 字段添加到数据库表中.
|
||||
|
||||
## 执行迁移步骤
|
||||
|
||||
### 1. 生成迁移文件
|
||||
|
||||
运行以下命令生成迁移文件:
|
||||
|
||||
```bash
|
||||
npm run migration:generate -- ./src/db/migrations/AddCoordinatesToArea
|
||||
```
|
||||
|
||||
### 2. 检查迁移文件
|
||||
|
||||
生成的迁移文件会自动包含添加 `latitude` 和 `longitude` 字段的SQL语句.您可以检查文件内容确保迁移逻辑正确.
|
||||
|
||||
### 3. 执行迁移
|
||||
|
||||
运行以下命令执行迁移,将更改应用到数据库:
|
||||
|
||||
```bash
|
||||
npm run migration:run
|
||||
```
|
||||
|
||||
## 手动迁移SQL(可选)
|
||||
|
||||
如果需要手动执行SQL,可以使用以下语句:
|
||||
|
||||
```sql
|
||||
ALTER TABLE `area`
|
||||
ADD COLUMN `latitude` DECIMAL(10,6) NULL AFTER `name`,
|
||||
ADD COLUMN `longitude` DECIMAL(10,6) NULL AFTER `latitude`;
|
||||
```
|
||||
|
||||
## 回滚迁移(如需)
|
||||
|
||||
如果遇到问题,可以使用以下命令回滚迁移:
|
||||
|
||||
```bash
|
||||
npm run typeorm -- migration:revert -d src/db/datasource.ts
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
|
||||
- 确保在执行迁移前备份数据库
|
||||
- 迁移不会影响现有数据,新增字段默认为 NULL
|
||||
- 迁移后,可以通过API开始为区域添加坐标信息
|
||||
|
|
@ -524,9 +524,9 @@
|
|||
}
|
||||
},
|
||||
"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==",
|
||||
"version": "10.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-10.2.0.tgz",
|
||||
"integrity": "sha512-rTXwAsIxpCqzUnZvrxVh3L0QA0NzToqWBLAhV+zDV3MIIwiQhAZHMdPCIaj5n/yADu/tyk12wIPgL6YHGXJP+g==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -7,19 +7,25 @@ export default {
|
|||
// dataSource: {
|
||||
// default: {
|
||||
// host: '13.212.62.127',
|
||||
// port: '3306',
|
||||
// username: 'root',
|
||||
// password: 'Yoone!@.2025',
|
||||
// database: 'inventory_v2',
|
||||
// synchronize: true,
|
||||
// logging: true,
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
typeorm: {
|
||||
typeorm: {
|
||||
dataSource: {
|
||||
default: {
|
||||
host: 'localhost',
|
||||
host: '13.212.62.127',
|
||||
port: "3306",
|
||||
username: 'root',
|
||||
password: 'root',
|
||||
database: 'inventory',
|
||||
password: 'Yoone!@.2025',
|
||||
database: 'inventory_v2',
|
||||
synchronize: true,
|
||||
logging: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -99,7 +99,7 @@ export class MainConfiguration {
|
|||
}
|
||||
|
||||
/**
|
||||
* 初始化数据库(如果不存在则创建)
|
||||
* 初始化数据库(如果不存在则创建)
|
||||
*/
|
||||
private async initializeDatabase(): Promise<void> {
|
||||
// 使用注入的数据库配置
|
||||
|
|
@ -118,8 +118,7 @@ export class MainConfiguration {
|
|||
});
|
||||
|
||||
try {
|
||||
this.logger.info('正在检查数据库是否存在...');
|
||||
|
||||
this.logger.info(`正在检查数据库是否存在...`+ JSON.stringify(typeormConfig));
|
||||
// 初始化临时数据源
|
||||
await tempDataSource.initialize();
|
||||
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ export class DictController {
|
|||
// 从上传的文件列表中获取第一个文件
|
||||
const file = files[0];
|
||||
// 调用服务层方法处理XLSX文件
|
||||
const result = await this.dictService.importDictsFromXLSX(file.data);
|
||||
const result = await this.dictService.importDictsFromTable(file.data);
|
||||
// 返回导入结果
|
||||
return result;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -79,6 +79,31 @@ export class ProductController {
|
|||
}
|
||||
}
|
||||
|
||||
@ApiOkResponse({
|
||||
description: '成功返回分组后的产品列表',
|
||||
schema: {
|
||||
type: 'object',
|
||||
additionalProperties: {
|
||||
type: 'array',
|
||||
items: {
|
||||
$ref: '#/components/schemas/Product',
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
@Get('/list/grouped')
|
||||
async getProductListGrouped(
|
||||
@Query() query: UnifiedSearchParamsDTO<ProductWhereFilter>
|
||||
): Promise<any> {
|
||||
try {
|
||||
const data = await this.productService.getProductListGrouped(query);
|
||||
return successResponse(data);
|
||||
} catch (error) {
|
||||
this.logger.error('获取分组产品列表失败', error);
|
||||
return errorResponse(error?.message || error);
|
||||
}
|
||||
}
|
||||
|
||||
@ApiOkResponse({ type: ProductRes })
|
||||
@Post('/')
|
||||
async createProduct(@Body() productData: CreateProductDTO) {
|
||||
|
|
@ -117,7 +142,7 @@ export class ProductController {
|
|||
const file = files?.[0];
|
||||
if (!file) return errorResponse('未接收到上传文件');
|
||||
|
||||
const result = await this.productService.importProductsCSV(file);
|
||||
const result = await this.productService.importProductsFromTable(file);
|
||||
return successResponse(result);
|
||||
} catch (error) {
|
||||
return errorResponse(error?.message || error);
|
||||
|
|
@ -698,10 +723,10 @@ export class ProductController {
|
|||
// 从站点同步产品到本地
|
||||
@ApiOkResponse({ description: '从站点同步产品到本地', type: ProductRes })
|
||||
@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 {
|
||||
const { siteId, siteProductId } = body;
|
||||
const product = await this.productService.syncProductFromSite(siteId, siteProductId);
|
||||
const { siteId, siteProductId, sku } = body;
|
||||
const product = await this.productService.syncProductFromSite(siteId, siteProductId, sku);
|
||||
return successResponse(product);
|
||||
} catch (error) {
|
||||
return errorResponse(error?.message || error);
|
||||
|
|
@ -713,25 +738,26 @@ export class ProductController {
|
|||
@Post('/batch-sync-from-site')
|
||||
async batchSyncFromSite(@Body() body: { siteId: number; siteProductIds: (string | number)[] }) {
|
||||
try {
|
||||
const { siteId, siteProductIds } = body;
|
||||
const result = await this.productService.batchSyncFromSite(siteId, siteProductIds);
|
||||
// 将服务层返回的结果转换为统一格式
|
||||
const errors = result.errors.map((error: string) => {
|
||||
// 提取产品ID部分作为标识符
|
||||
const match = error.match(/站点产品ID (\d+) /);
|
||||
const identifier = match ? match[1] : 'unknown';
|
||||
return {
|
||||
identifier: identifier,
|
||||
error: error
|
||||
};
|
||||
});
|
||||
throw new Error('批量同步产品到本地暂未实现');
|
||||
// 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 match = error.match(/站点产品ID (\d+) /);
|
||||
// const identifier = match ? match[1] : 'unknown';
|
||||
// return {
|
||||
// identifier: identifier,
|
||||
// error: error
|
||||
// };
|
||||
// });
|
||||
|
||||
return successResponse({
|
||||
total: siteProductIds.length,
|
||||
processed: result.synced + errors.length,
|
||||
synced: result.synced,
|
||||
errors: errors
|
||||
});
|
||||
// return successResponse({
|
||||
// total: siteProductIds.length,
|
||||
// processed: result.synced + errors.length,
|
||||
// synced: result.synced,
|
||||
// errors: errors
|
||||
// });
|
||||
} catch (error) {
|
||||
return errorResponse(error?.message || error);
|
||||
}
|
||||
|
|
@ -749,4 +775,31 @@ export class ProductController {
|
|||
return errorResponse(error?.message || error);
|
||||
}
|
||||
}
|
||||
|
||||
// 获取所有产品,支持按品牌过滤
|
||||
@ApiOkResponse({ description: '获取所有产品', type: ProductListRes })
|
||||
@Get('/all')
|
||||
async getAllProducts(@Query('brand') brand?: string) {
|
||||
try {
|
||||
const data = await this.productService.getAllProducts(brand);
|
||||
return successResponse(data);
|
||||
} catch (error) {
|
||||
return errorResponse(error?.message || error);
|
||||
}
|
||||
}
|
||||
|
||||
// 获取按属性分组的产品,默认按强度划分
|
||||
@ApiOkResponse({ description: '获取按属性分组的产品' })
|
||||
@Get('/grouped')
|
||||
async getGroupedProducts(
|
||||
@Query('brand') brand?: string,
|
||||
@Query('attribute') attribute: string = 'strength'
|
||||
) {
|
||||
try {
|
||||
const data = await this.productService.getProductsGroupedByAttribute(brand, attribute);
|
||||
return successResponse(data);
|
||||
} catch (error) {
|
||||
return errorResponse(error?.message || error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ import {
|
|||
CancelFulfillmentDTO,
|
||||
CreateReviewDTO,
|
||||
CreateWebhookDTO,
|
||||
FulfillmentDTO,
|
||||
UnifiedCustomerDTO,
|
||||
UnifiedCustomerPaginationDTO,
|
||||
UnifiedMediaPaginationDTO,
|
||||
|
|
@ -106,7 +105,7 @@ export class SiteApiController {
|
|||
this.logger.debug(`[Site API] 更新评论开始, siteId: ${siteId}, id: ${id}, body: ${JSON.stringify(body)}`);
|
||||
try {
|
||||
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}`);
|
||||
return successResponse(data);
|
||||
} catch (error) {
|
||||
|
|
@ -124,7 +123,7 @@ export class SiteApiController {
|
|||
this.logger.debug(`[Site API] 删除评论开始, siteId: ${siteId}, id: ${id}`);
|
||||
try {
|
||||
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}`);
|
||||
return successResponse(data);
|
||||
} catch (error) {
|
||||
|
|
@ -160,7 +159,7 @@ export class SiteApiController {
|
|||
this.logger.debug(`[Site API] 获取单个webhook开始, siteId: ${siteId}, id: ${id}`);
|
||||
try {
|
||||
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}`);
|
||||
return successResponse(data);
|
||||
} catch (error) {
|
||||
|
|
@ -199,7 +198,7 @@ export class SiteApiController {
|
|||
this.logger.debug(`[Site API] 更新webhook开始, siteId: ${siteId}, id: ${id}, body: ${JSON.stringify(body)}`);
|
||||
try {
|
||||
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}`);
|
||||
return successResponse(data);
|
||||
} catch (error) {
|
||||
|
|
@ -217,7 +216,7 @@ export class SiteApiController {
|
|||
this.logger.debug(`[Site API] 删除webhook开始, siteId: ${siteId}, id: ${id}`);
|
||||
try {
|
||||
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}`);
|
||||
return successResponse(data);
|
||||
} catch (error) {
|
||||
|
|
@ -300,7 +299,7 @@ export class SiteApiController {
|
|||
}
|
||||
}
|
||||
|
||||
// 平台特性:产品导出(特殊CSV,走平台服务)
|
||||
// 平台特性:产品导出(特殊CSV,走平台服务)
|
||||
@Get('/:siteId/links')
|
||||
async getLinks(
|
||||
@Param('siteId') siteId: number
|
||||
|
|
@ -327,7 +326,7 @@ export class SiteApiController {
|
|||
if (site.type === 'woocommerce') {
|
||||
const page = query.page || 1;
|
||||
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 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) => {
|
||||
|
|
@ -360,7 +359,7 @@ export class SiteApiController {
|
|||
this.logger.info(`[Site API] 获取单个产品开始, siteId: ${siteId}, productId: ${id}`);
|
||||
try {
|
||||
const adapter = await this.siteApiService.getAdapter(siteId);
|
||||
const data = await adapter.getProduct(id);
|
||||
const data = await adapter.getProduct({ id });
|
||||
|
||||
// 如果获取到商品数据,则增强ERP产品信息
|
||||
if (data) {
|
||||
|
|
@ -430,7 +429,7 @@ export class SiteApiController {
|
|||
}
|
||||
}
|
||||
|
||||
// 平台特性:产品导入(特殊CSV,走平台服务)
|
||||
// 平台特性:产品导入(特殊CSV,走平台服务)
|
||||
@Post('/:siteId/products/import-special')
|
||||
@ApiOkResponse({ type: Object })
|
||||
async importProductsSpecial(
|
||||
|
|
@ -444,7 +443,7 @@ export class SiteApiController {
|
|||
const created: any[] = [];
|
||||
const failed: any[] = [];
|
||||
if (site.type === 'woocommerce') {
|
||||
// 解析 CSV 为对象数组(若传入 items 则优先 items)
|
||||
// 解析 CSV 为对象数组(若传入 items 则优先 items)
|
||||
let payloads = items;
|
||||
if (!payloads.length && csvText) {
|
||||
const lines = csvText.split(/\r?\n/).filter(Boolean);
|
||||
|
|
@ -485,7 +484,7 @@ export class SiteApiController {
|
|||
this.logger.info(`[Site API] 更新产品开始, siteId: ${siteId}, productId: ${id}`);
|
||||
try {
|
||||
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}`);
|
||||
return successResponse(data);
|
||||
} catch (error) {
|
||||
|
|
@ -540,7 +539,7 @@ export class SiteApiController {
|
|||
this.logger.info(`[Site API] 删除产品开始, siteId: ${siteId}, productId: ${id}`);
|
||||
try {
|
||||
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}`);
|
||||
return successResponse(success);
|
||||
} catch (error) {
|
||||
|
|
@ -585,7 +584,7 @@ export class SiteApiController {
|
|||
for (const item of body.update) {
|
||||
try {
|
||||
const id = item.id;
|
||||
const data = await adapter.updateProduct(id, item);
|
||||
const data = await adapter.updateProduct({ id }, item);
|
||||
updated.push(data);
|
||||
} catch (e) {
|
||||
errors.push({
|
||||
|
|
@ -598,7 +597,7 @@ export class SiteApiController {
|
|||
if (body.delete?.length) {
|
||||
for (const id of body.delete) {
|
||||
try {
|
||||
const ok = await adapter.deleteProduct(id);
|
||||
const ok = await adapter.deleteProduct({ id });
|
||||
if (ok) deleted.push(id);
|
||||
else errors.push({
|
||||
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')
|
||||
@ApiOkResponse({ type: UnifiedOrderPaginationDTO })
|
||||
async getCustomerOrders(
|
||||
|
|
@ -752,7 +771,7 @@ export class SiteApiController {
|
|||
this.logger.info(`[Site API] 获取单个订单开始, siteId: ${siteId}, orderId: ${id}`);
|
||||
try {
|
||||
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}`);
|
||||
return successResponse(data);
|
||||
} catch (error) {
|
||||
|
|
@ -824,7 +843,7 @@ export class SiteApiController {
|
|||
this.logger.info(`[Site API] 更新订单开始, siteId: ${siteId}, orderId: ${id}`);
|
||||
try {
|
||||
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}`);
|
||||
return successResponse(ok);
|
||||
} catch (error) {
|
||||
|
|
@ -842,7 +861,7 @@ export class SiteApiController {
|
|||
this.logger.info(`[Site API] 删除订单开始, siteId: ${siteId}, orderId: ${id}`);
|
||||
try {
|
||||
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}`);
|
||||
return successResponse(ok);
|
||||
} catch (error) {
|
||||
|
|
@ -882,7 +901,7 @@ export class SiteApiController {
|
|||
for (const item of body.update) {
|
||||
try {
|
||||
const id = item.id;
|
||||
const ok = await adapter.updateOrder(id, item);
|
||||
const ok = await adapter.updateOrder({ id }, item);
|
||||
if (ok) updated.push(item);
|
||||
else errors.push({
|
||||
identifier: String(item.id || 'unknown'),
|
||||
|
|
@ -899,7 +918,7 @@ export class SiteApiController {
|
|||
if (body.delete?.length) {
|
||||
for (const id of body.delete) {
|
||||
try {
|
||||
const ok = await adapter.deleteOrder(id);
|
||||
const ok = await adapter.deleteOrder({ id });
|
||||
if (ok) deleted.push(id);
|
||||
else errors.push({
|
||||
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')
|
||||
@ApiOkResponse({ type: Object })
|
||||
async cancelFulfillment(
|
||||
|
|
@ -1050,13 +1050,13 @@ export class SiteApiController {
|
|||
}
|
||||
}
|
||||
|
||||
@Get('/:siteId/orders/:orderId/trackings')
|
||||
@Get('/:siteId/orders/:orderId/fulfillments')
|
||||
@ApiOkResponse({ type: Object })
|
||||
async getOrderTrackings(
|
||||
async getOrderFulfillments(
|
||||
@Param('siteId') siteId: number,
|
||||
@Param('orderId') orderId: string
|
||||
) {
|
||||
this.logger.info(`[Site API] 获取订单物流跟踪信息开始, siteId: ${siteId}, orderId: ${orderId}`);
|
||||
this.logger.info(`[Site API] 获取订单履约信息开始, siteId: ${siteId}, orderId: ${orderId}`);
|
||||
try {
|
||||
const adapter = await this.siteApiService.getAdapter(siteId);
|
||||
const data = await adapter.getOrderFulfillments(orderId);
|
||||
|
|
@ -1435,7 +1435,7 @@ export class SiteApiController {
|
|||
this.logger.info(`[Site API] 获取单个客户开始, siteId: ${siteId}, customerId: ${id}`);
|
||||
try {
|
||||
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}`);
|
||||
return successResponse(data);
|
||||
} catch (error) {
|
||||
|
|
@ -1507,7 +1507,7 @@ export class SiteApiController {
|
|||
this.logger.info(`[Site API] 更新客户开始, siteId: ${siteId}, customerId: ${id}`);
|
||||
try {
|
||||
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}`);
|
||||
return successResponse(data);
|
||||
} catch (error) {
|
||||
|
|
@ -1525,7 +1525,7 @@ export class SiteApiController {
|
|||
this.logger.info(`[Site API] 删除客户开始, siteId: ${siteId}, customerId: ${id}`);
|
||||
try {
|
||||
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}`);
|
||||
return successResponse(success);
|
||||
} catch (error) {
|
||||
|
|
@ -1561,7 +1561,7 @@ export class SiteApiController {
|
|||
for (const item of body.update) {
|
||||
try {
|
||||
const id = item.id;
|
||||
const data = await adapter.updateCustomer(id, item);
|
||||
const data = await adapter.updateCustomer({ id }, item);
|
||||
updated.push(data);
|
||||
} catch (e) {
|
||||
failed.push({ action: 'update', item, error: (e as any).message });
|
||||
|
|
@ -1571,7 +1571,7 @@ export class SiteApiController {
|
|||
if (body.delete?.length) {
|
||||
for (const id of body.delete) {
|
||||
try {
|
||||
const ok = await adapter.deleteCustomer(id);
|
||||
const ok = await adapter.deleteCustomer({ id });
|
||||
if (ok) deleted.push(id);
|
||||
else failed.push({ action: 'delete', id, error: 'delete failed' });
|
||||
} catch (e) {
|
||||
|
|
|
|||
|
|
@ -79,7 +79,7 @@ export class StatisticsController {
|
|||
|
||||
@ApiOkResponse()
|
||||
@Get('/orderSource')
|
||||
async getOrderSorce(@Query() params) {
|
||||
async getOrderSource(@Query() params) {
|
||||
try {
|
||||
return successResponse(await this.statisticsService.getOrderSorce(params));
|
||||
} catch (error) {
|
||||
|
|
|
|||
|
|
@ -9,13 +9,12 @@ import {
|
|||
} from '@midwayjs/decorator';
|
||||
import { Context } from '@midwayjs/koa';
|
||||
import * as crypto from 'crypto';
|
||||
|
||||
|
||||
import { SiteService } from '../service/site.service';
|
||||
import { OrderService } from '../service/order.service';
|
||||
import { SiteApiService } from '../service/site-api.service';
|
||||
|
||||
|
||||
import {
|
||||
UnifiedOrderDTO,
|
||||
} from '../dto/site-api.dto';
|
||||
|
||||
@Controller('/webhook')
|
||||
export class WebhookController {
|
||||
|
|
@ -31,9 +30,11 @@ export class WebhookController {
|
|||
|
||||
@Logger()
|
||||
logger: ILogger;
|
||||
|
||||
|
||||
@Inject()
|
||||
private readonly siteService: SiteService;
|
||||
@Inject()
|
||||
private readonly siteApiService: SiteApiService;
|
||||
|
||||
// 移除配置中的站点数组,来源统一改为数据库
|
||||
|
||||
|
|
@ -49,7 +50,7 @@ export class WebhookController {
|
|||
@Query('siteId') siteIdStr: string,
|
||||
@Headers() header: any
|
||||
) {
|
||||
console.log(`webhook woocommerce`, siteIdStr, body,header)
|
||||
console.log(`webhook woocommerce`, siteIdStr, body, header)
|
||||
const signature = header['x-wc-webhook-signature'];
|
||||
const topic = header['x-wc-webhook-topic'];
|
||||
const source = header['x-wc-webhook-source'];
|
||||
|
|
@ -79,43 +80,44 @@ export class WebhookController {
|
|||
.update(rawBody)
|
||||
.digest('base64');
|
||||
try {
|
||||
if (hash === signature) {
|
||||
switch (topic) {
|
||||
case 'product.created':
|
||||
case 'product.updated':
|
||||
// 不再写入本地,平台事件仅确认接收
|
||||
break;
|
||||
case 'product.deleted':
|
||||
// 不再写入本地,平台事件仅确认接收
|
||||
break;
|
||||
case 'order.created':
|
||||
case 'order.updated':
|
||||
await this.orderService.syncSingleOrder(siteId, body);
|
||||
break;
|
||||
case 'order.deleted':
|
||||
break;
|
||||
case 'customer.created':
|
||||
break;
|
||||
case 'customer.updated':
|
||||
break;
|
||||
case 'customer.deleted':
|
||||
break;
|
||||
default:
|
||||
console.log('Unhandled event:', body.event);
|
||||
}
|
||||
|
||||
return {
|
||||
code: 200,
|
||||
success: true,
|
||||
message: 'Webhook processed successfully',
|
||||
};
|
||||
} else {
|
||||
if (hash !== signature) {
|
||||
return {
|
||||
code: 403,
|
||||
success: false,
|
||||
message: 'Webhook verification failed',
|
||||
};
|
||||
}
|
||||
const adapter = await this.siteApiService.getAdapter(siteId);
|
||||
switch (topic) {
|
||||
case 'product.created':
|
||||
case 'product.updated':
|
||||
// 不再写入本地,平台事件仅确认接收
|
||||
break;
|
||||
case 'product.deleted':
|
||||
// 不再写入本地,平台事件仅确认接收
|
||||
break;
|
||||
case 'order.created':
|
||||
case 'order.updated':
|
||||
const order = adapter.mapPlatformToUnifiedOrder(body)
|
||||
await this.orderService.syncSingleOrder(siteId, order);
|
||||
break;
|
||||
case 'order.deleted':
|
||||
break;
|
||||
case 'customer.created':
|
||||
break;
|
||||
case 'customer.updated':
|
||||
break;
|
||||
case 'customer.deleted':
|
||||
break;
|
||||
default:
|
||||
console.log('Unhandled event:', body.event);
|
||||
|
||||
return {
|
||||
code: 200,
|
||||
success: true,
|
||||
message: 'Webhook processed successfully',
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
|
|
@ -130,23 +132,10 @@ export class WebhookController {
|
|||
@Query('signature') signature: string,
|
||||
@Headers() header: any
|
||||
) {
|
||||
console.log(`webhook shoppy`, siteIdStr, body, header)
|
||||
const topic = header['x-oemsaas-event-type'];
|
||||
// const source = header['x-oemsaas-shop-domain'];
|
||||
// const source = header['x-oemsaas-shop-domain'];
|
||||
const siteId = Number(siteIdStr);
|
||||
const bodys = new UnifiedOrderDTO();
|
||||
Object.assign(bodys, body);
|
||||
// 从数据库获取站点配置
|
||||
const site = await this.siteService.get(siteId, true);
|
||||
|
||||
// if (!site || !source?.includes(site.websiteUrl)) {
|
||||
if (!site) {
|
||||
console.log('domain not match');
|
||||
return {
|
||||
code: HttpStatus.BAD_REQUEST,
|
||||
success: false,
|
||||
message: 'domain not match',
|
||||
};
|
||||
}
|
||||
|
||||
if (!signature) {
|
||||
return {
|
||||
|
|
@ -162,6 +151,7 @@ export class WebhookController {
|
|||
// .createHmac('sha256', this.secret)
|
||||
// .update(rawBody)
|
||||
// .digest('base64');
|
||||
const adapter = await this.siteApiService.getAdapter(siteId);
|
||||
try {
|
||||
if (this.secret === signature) {
|
||||
switch (topic) {
|
||||
|
|
@ -174,7 +164,8 @@ export class WebhookController {
|
|||
break;
|
||||
case 'orders/create':
|
||||
case 'orders/update':
|
||||
await this.orderService.syncSingleOrder(siteId, bodys);
|
||||
const order = adapter.mapPlatformToUnifiedOrder(body)
|
||||
await this.orderService.syncSingleOrder(siteId, order);
|
||||
break;
|
||||
case 'orders/delete':
|
||||
break;
|
||||
|
|
@ -188,20 +179,15 @@ export class WebhookController {
|
|||
console.log('Unhandled event:', topic);
|
||||
}
|
||||
|
||||
return {
|
||||
code: 200,
|
||||
success: true,
|
||||
message: 'Webhook processed successfully',
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
code: 403,
|
||||
success: false,
|
||||
message: 'Webhook verification failed',
|
||||
};
|
||||
return {
|
||||
code: 200,
|
||||
success: true,
|
||||
message: 'Webhook processed successfully',
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -126,7 +126,7 @@ const flavorsData = [
|
|||
{ name: 'arctic-mint', title: 'arctic mint', titleCn: '北极薄荷', shortName: 'AR' },
|
||||
{ name: 'baddie-blueberries', title: 'baddie blueberries', titleCn: '时髦蓝莓', shortName: 'BA' },
|
||||
{ name: 'banana', title: 'banana', titleCn: '香蕉', shortName: 'BA' },
|
||||
{ name: 'banana-(solid)', title: 'banana (solid)', titleCn: '香蕉(固体)', shortName: 'BA' },
|
||||
{ name: 'banana-(solid)', title: 'banana (solid)', titleCn: '香蕉(固体)', shortName: 'BA' },
|
||||
{ name: 'banana-berry', title: 'banana berry', titleCn: '香蕉莓果', shortName: 'BA' },
|
||||
{ name: 'banana-berry-melon-ice', title: 'banana berry melon ice', titleCn: '香蕉莓果瓜冰', shortName: 'BA' },
|
||||
{ name: 'banana-blackberry', title: 'banana blackberry', titleCn: '香蕉黑莓', shortName: 'BA' },
|
||||
|
|
@ -137,7 +137,7 @@ const flavorsData = [
|
|||
{ name: 'bangin-blood-orange-iced', title: 'bangin blood orange iced', titleCn: '爆炸血橙冰', shortName: 'BA' },
|
||||
{ name: 'berries-in-the-6ix', title: 'berries in the 6ix', titleCn: '多伦多莓果', shortName: 'BE' },
|
||||
{ name: 'berry-burst', title: 'berry burst', titleCn: '浆果爆发', shortName: 'BE' },
|
||||
{ name: 'berry-burst-(thermal)', title: 'berry burst (thermal)', titleCn: '浆果爆发(热感)', shortName: 'BE' },
|
||||
{ name: 'berry-burst-(thermal)', title: 'berry burst (thermal)', titleCn: '浆果爆发(热感)', shortName: 'BE' },
|
||||
{ name: 'berry-ice', title: 'berry ice', titleCn: '浆果冰', shortName: 'BE' },
|
||||
{ name: 'berry-lime-ice', title: 'berry lime ice', titleCn: '浆果青柠冰', shortName: 'BE' },
|
||||
{ name: 'berry-trio-ice', title: 'berry trio ice', titleCn: '三重浆果冰', shortName: 'BE' },
|
||||
|
|
@ -145,7 +145,7 @@ const flavorsData = [
|
|||
{ name: 'black-cherry', title: 'black cherry', titleCn: '黑樱桃', shortName: 'BL' },
|
||||
{ name: 'blackcherry', title: 'blackcherry', titleCn: '黑樱桃混合', shortName: 'BL' },
|
||||
{ name: 'blackcurrant-ice', title: 'blackcurrant ice', titleCn: '黑加仑冰', shortName: 'BL' },
|
||||
{ name: 'black-currant-ice', title: 'black currant ice', titleCn: '黑加仑冰(空格版)', shortName: 'BL' },
|
||||
{ name: 'black-currant-ice', title: 'black currant ice', titleCn: '黑加仑冰(空格版)', shortName: 'BL' },
|
||||
{ name: 'black-licorice', title: 'black licorice', titleCn: '黑甘草', shortName: 'BL' },
|
||||
{ name: 'black-tea', title: 'black tea', titleCn: '红茶', shortName: 'BL' },
|
||||
{ name: 'blackberry-ice', title: 'blackberry ice', titleCn: '黑莓冰', shortName: 'BL' },
|
||||
|
|
@ -168,7 +168,7 @@ const flavorsData = [
|
|||
{ name: 'blue-razz', title: 'blue razz', titleCn: '蓝覆盆子', shortName: 'BL' },
|
||||
{ name: 'blue-razz-hype', title: 'blue razz hype', titleCn: '蓝覆盆子热情', shortName: 'BL' },
|
||||
{ name: 'blue-razz-ice', title: 'blue razz ice', titleCn: '蓝覆盆子冰', shortName: 'BL' },
|
||||
{ name: 'blue-razz-ice-(solid)', title: 'blue razz ice (solid)', titleCn: '蓝覆盆子冰(固体)', shortName: 'BL' },
|
||||
{ name: 'blue-razz-ice-(solid)', title: 'blue razz ice (solid)', titleCn: '蓝覆盆子冰(固体)', shortName: 'BL' },
|
||||
{ name: 'blue-razz-ice-glace', title: 'blue razz ice glace', titleCn: '蓝覆盆子冰格', shortName: 'BL' },
|
||||
{ name: 'blue-razz-lemon-ice', title: 'blue razz lemon ice', titleCn: '蓝覆盆子柠檬冰', shortName: 'BL' },
|
||||
{ name: 'blue-razz-lemonade', title: 'blue razz lemonade', titleCn: '蓝覆盆子柠檬水', shortName: 'BL' },
|
||||
|
|
@ -196,7 +196,7 @@ const flavorsData = [
|
|||
{ name: 'bumpin-blackcurrant-iced', title: 'bumpin blackcurrant iced', titleCn: '黑加仑热烈冰', shortName: 'BU' },
|
||||
{ name: 'burst-ice', title: 'burst ice', titleCn: '爆炸冰', shortName: 'BU' },
|
||||
{ name: 'bussin-banana-iced', title: 'bussin banana iced', titleCn: '香蕉热烈冰', shortName: 'BU' },
|
||||
{ name: 'bussin-banana-iced', title: 'bussin banana iced', titleCn: '香蕉热烈冰(重复)', shortName: 'BU' },
|
||||
{ name: 'bussin-banana-iced', title: 'bussin banana iced', titleCn: '香蕉热烈冰(重复)', shortName: 'BU' },
|
||||
{ name: 'california-cherry', title: 'california cherry', titleCn: '加州樱桃', shortName: 'CA' },
|
||||
{ name: 'cantaloupe-mango-banana', title: 'cantaloupe mango banana', titleCn: '香瓜芒果香蕉', shortName: 'CA' },
|
||||
{ name: 'caramel', title: 'caramel', titleCn: '焦糖', shortName: 'CA' },
|
||||
|
|
@ -230,7 +230,7 @@ const flavorsData = [
|
|||
{ name: 'citrus-chill', title: 'citrus chill', titleCn: '柑橘清凉', shortName: 'CI' },
|
||||
{ name: 'citrus-smash-ice', title: 'citrus smash ice', titleCn: '柑橘冲击冰', shortName: 'CI' },
|
||||
{ name: 'citrus-sunrise', title: 'citrus sunrise', titleCn: '柑橘日出', shortName: 'CI' },
|
||||
{ name: 'citrus-sunrise-(thermal)', title: 'citrus sunrise (thermal)', titleCn: '柑橘日出(热感)', shortName: 'CI' },
|
||||
{ name: 'citrus-sunrise-(thermal)', title: 'citrus sunrise (thermal)', titleCn: '柑橘日出(热感)', shortName: 'CI' },
|
||||
{ name: 'classic', title: 'classic', titleCn: '经典', shortName: 'CL' },
|
||||
{ name: 'classic-ice', title: 'classic ice', titleCn: '经典冰', shortName: 'CL' },
|
||||
{ name: 'classic-mint-ice', title: 'classic mint ice', titleCn: '经典薄荷冰', shortName: 'CL' },
|
||||
|
|
@ -310,7 +310,7 @@ const flavorsData = [
|
|||
{ name: 'fizzy', title: 'fizzy', titleCn: '汽水', shortName: 'FI' },
|
||||
{ name: 'flavourless', title: 'flavourless', titleCn: '无味', shortName: 'FL' },
|
||||
{ name: 'flippin-fruit-flash', title: 'flippin fruit flash', titleCn: '翻转水果闪电', shortName: 'FL' },
|
||||
{ name: 'flippin-fruit-flash-(rainbow-burst)', title: 'flippin fruit flash (rainbow burst)', titleCn: '翻转水果闪电(彩虹爆发)', shortName: 'FL' },
|
||||
{ name: 'flippin-fruit-flash-(rainbow-burst)', title: 'flippin fruit flash (rainbow burst)', titleCn: '翻转水果闪电(彩虹爆发)', shortName: 'FL' },
|
||||
{ name: 'forest-fruits', title: 'forest fruits', titleCn: '森林水果', shortName: 'FO' },
|
||||
{ name: 'fragrant-grapefruit', title: 'fragrant grapefruit', titleCn: '香气葡萄柚', shortName: 'FR' },
|
||||
{ name: 'freeze', title: 'freeze', titleCn: '冰冻', shortName: 'FR' },
|
||||
|
|
@ -340,14 +340,14 @@ const flavorsData = [
|
|||
{ name: 'fuji-melon-ice', title: 'fuji melon ice', titleCn: '富士瓜冰', shortName: 'FU' },
|
||||
{ name: 'full-charge', title: 'full charge', titleCn: '满电', shortName: 'FU' },
|
||||
{ name: 'gb', title: 'gb', titleCn: '软糖', shortName: 'GB' },
|
||||
{ name: 'gb(gummy-bear)', title: 'gb(gummy bear)', titleCn: '软糖(Gummy Bear)', shortName: 'GB' },
|
||||
{ name: 'gb(gummy-bear)', title: 'gb(gummy bear)', titleCn: '软糖(Gummy Bear)', shortName: 'GB' },
|
||||
{ name: 'gentle-mint', title: 'gentle mint', titleCn: '温和薄荷', shortName: 'GE' },
|
||||
{ name: 'ghost-cola-&-vanilla', title: 'ghost cola & vanilla', titleCn: '幽灵可乐香草', shortName: 'GH' },
|
||||
{ name: 'ghost-cola-ice', title: 'ghost cola ice', titleCn: '幽灵可乐冰', shortName: 'GH' },
|
||||
{ name: 'ghost-mango', title: 'ghost mango', titleCn: '幽灵芒果', shortName: 'GH' },
|
||||
{ name: 'ghost-original', title: 'ghost original', titleCn: '幽灵原味', shortName: 'GH' },
|
||||
{ name: 'ghost-watermelon-ice', title: 'ghost watermelon ice', titleCn: '幽灵西瓜冰', shortName: 'GH' },
|
||||
{ name: 'gnarly-green-d-(green-dew)', title: 'gnarly green d (green dew)', titleCn: '狂野绿 D(绿色露水)', shortName: 'GN' },
|
||||
{ name: 'gnarly-green-d-(green-dew)', title: 'gnarly green d (green dew)', titleCn: '狂野绿 D(绿色露水)', shortName: 'GN' },
|
||||
{ name: 'gold-edition', title: 'gold edition', titleCn: '金版', shortName: 'GO' },
|
||||
{ name: 'grape', title: 'grape', titleCn: '葡萄', shortName: 'GR' },
|
||||
{ name: 'grape-cherry', title: 'grape cherry', titleCn: '葡萄樱桃', shortName: 'GR' },
|
||||
|
|
@ -492,13 +492,13 @@ const flavorsData = [
|
|||
{ name: 'mixed-fruit', title: 'mixed fruit', titleCn: '混合水果', shortName: 'MI' },
|
||||
{ name: 'mocha-ice', title: 'mocha ice', titleCn: '摩卡冰', shortName: 'MO' },
|
||||
{ name: 'morocco-mint', title: 'morocco mint', titleCn: '摩洛哥薄荷', shortName: 'MO' },
|
||||
{ name: 'morocco-mint-(thermal)', title: 'morocco mint (thermal)', titleCn: '摩洛哥薄荷(热感)', shortName: 'MO' },
|
||||
{ name: 'morocco-mint-(thermal)', title: 'morocco mint (thermal)', titleCn: '摩洛哥薄荷(热感)', shortName: 'MO' },
|
||||
{ name: 'mung-beans', title: 'mung beans', titleCn: '绿豆', shortName: 'MU' },
|
||||
{ name: 'nasty-tropic', title: 'nasty tropic', titleCn: '恶搞热带', shortName: 'NA' },
|
||||
{ name: 'nectarine-ice', title: 'nectarine ice', titleCn: '油桃冰', shortName: 'NE' },
|
||||
{ name: 'night-rider', title: 'night rider', titleCn: '夜骑', shortName: 'NI' },
|
||||
{ name: 'nirvana', title: 'nirvana', titleCn: '宁静蓝莓', shortName: 'NI' },
|
||||
{ name: 'north-american-style(root-beer)', title: 'north american style(root beer)', titleCn: '北美风格(根啤)', shortName: 'NO' },
|
||||
{ name: 'north-american-style(root-beer)', title: 'north american style(root beer)', titleCn: '北美风格(根啤)', shortName: 'NO' },
|
||||
{ name: 'northern-blue-razz', title: 'northern blue razz', titleCn: '北方蓝覆盆子', shortName: 'NO' },
|
||||
{ name: 'nutty-virginia', title: 'nutty virginia', titleCn: '坚果弗吉尼亚', shortName: 'NU' },
|
||||
{ name: 'orange', title: 'orange', titleCn: '橙子', shortName: 'OR' },
|
||||
|
|
@ -508,12 +508,12 @@ const flavorsData = [
|
|||
{ name: 'orange-mango-guava', title: 'orange mango guava', titleCn: '橙子芒果番石榴', shortName: 'OR' },
|
||||
{ name: 'orange-mango-pineapple-ice', title: 'orange mango pineapple ice', titleCn: '橙子芒果菠萝冰', shortName: 'OR' },
|
||||
{ name: 'orange-p', title: 'orange p', titleCn: '橙子 P', shortName: 'OR' },
|
||||
{ name: 'orange-p(fanta)', title: 'orange p(fanta)', titleCn: '橙子 P(芬达)', shortName: 'OR' },
|
||||
{ name: 'orange-p(fanta)', title: 'orange p(fanta)', titleCn: '橙子 P(芬达)', shortName: 'OR' },
|
||||
{ name: 'orange-spark', title: 'orange spark', titleCn: '橙色火花', shortName: 'OR' },
|
||||
{ name: 'orange-tangerine', title: 'orange tangerine', titleCn: '橙子柑橘', shortName: 'OR' },
|
||||
{ name: 'original', title: 'original', titleCn: '原味', shortName: 'OR' },
|
||||
{ name: 'packin-peach-berry', title: 'packin peach berry', titleCn: '装满桃浆果', shortName: 'PA' },
|
||||
{ name: 'packin-peach-berry-(popn-peach-berry)', title: 'packin peach berry (popn peach berry)', titleCn: '装满桃浆果(Pop’n 桃浆果)', shortName: 'PA' },
|
||||
{ name: 'packin-peach-berry-(popn-peach-berry)', title: 'packin peach berry (popn peach berry)', titleCn: '装满桃浆果(Pop’n 桃浆果)', shortName: 'PA' },
|
||||
{ name: 'papio', title: 'papio', titleCn: 'Papio', shortName: 'PA' },
|
||||
{ name: 'paradise', title: 'paradise', titleCn: '天堂', shortName: 'PA' },
|
||||
{ name: 'paradise-iced', title: 'paradise iced', titleCn: '天堂冰', shortName: 'PA' },
|
||||
|
|
@ -603,7 +603,7 @@ const flavorsData = [
|
|||
{ name: 'red-fruits', title: 'red fruits', titleCn: '红色水果', shortName: 'RE' },
|
||||
{ name: 'red-lightning', title: 'red lightning', titleCn: '红色闪电', shortName: 'RE' },
|
||||
{ name: 'red-line', title: 'red line', titleCn: '红线', shortName: 'RE' },
|
||||
{ name: 'red-line-(energy-drink)', title: 'red line (energy drink)', titleCn: '红线(能量饮料)', shortName: 'RE' },
|
||||
{ name: 'red-line-(energy-drink)', title: 'red line (energy drink)', titleCn: '红线(能量饮料)', shortName: 'RE' },
|
||||
{ name: 'red-magic', title: 'red magic', titleCn: '红魔', shortName: 'RE' },
|
||||
{ name: 'rich-tobacco', title: 'rich tobacco', titleCn: '浓烈烟草', shortName: 'RI' },
|
||||
{ name: 'root-beer', title: 'root beer', titleCn: '根啤', shortName: 'RO' },
|
||||
|
|
@ -625,8 +625,8 @@ const flavorsData = [
|
|||
{ name: 'sic-strawberry-iced', title: 'sic strawberry iced', titleCn: '意大利草莓冰', shortName: 'SI' },
|
||||
{ name: 'simply-spearmint', title: 'simply spearmint', titleCn: '清爽留兰香', shortName: 'SI' },
|
||||
{ name: 'skc', title: 'skc', titleCn: 'SKC', shortName: 'SK' },
|
||||
{ name: 'skc(skittles-candy)', title: 'skc(skittles candy)', titleCn: 'SKC(彩虹糖)', shortName: 'SK' },
|
||||
{ name: 'slammin-sts-(sour-snap)', title: 'slammin sts (sour snap)', titleCn: '热烈 STS(酸糖)', shortName: 'SL' },
|
||||
{ name: 'skc(skittles-candy)', title: 'skc(skittles candy)', titleCn: 'SKC(彩虹糖)', shortName: 'SK' },
|
||||
{ name: 'slammin-sts-(sour-snap)', title: 'slammin sts (sour snap)', titleCn: '热烈 STS(酸糖)', shortName: 'SL' },
|
||||
{ name: 'slammin-sts-iced', title: 'slammin sts iced', titleCn: '热烈 STS 冰', shortName: 'SL' },
|
||||
{ name: 'smooth', title: 'smooth', titleCn: '顺滑', shortName: 'SM' },
|
||||
{ name: 'smooth-mint', title: 'smooth mint', titleCn: '顺滑薄荷', shortName: 'SM' },
|
||||
|
|
@ -664,7 +664,7 @@ const flavorsData = [
|
|||
{ name: 'strawberry-jasmine-t', title: 'strawberry jasmine t', titleCn: '草莓茉莉茶', shortName: 'ST' },
|
||||
{ name: 'strawberry-jasmine-tea', title: 'strawberry jasmine tea', titleCn: '草莓茉莉茶', shortName: 'ST' },
|
||||
{ name: 'strawberry-kiwi', title: 'strawberry kiwi', titleCn: '草莓奇异果', shortName: 'ST' },
|
||||
{ name: 'strawberry-kiwi-(solid)', title: 'strawberry kiwi (solid)', titleCn: '草莓奇异果(固体)', shortName: 'ST' },
|
||||
{ name: 'strawberry-kiwi-(solid)', title: 'strawberry kiwi (solid)', titleCn: '草莓奇异果(固体)', shortName: 'ST' },
|
||||
{ name: 'strawberry-kiwi-banana-ice', title: 'strawberry kiwi banana ice', titleCn: '草莓奇异果香蕉冰', shortName: 'ST' },
|
||||
{ name: 'strawberry-kiwi-guava-ice', title: 'strawberry kiwi guava ice', titleCn: '草莓奇异果番石榴冰', shortName: 'ST' },
|
||||
{ name: 'strawberry-kiwi-ice', title: 'strawberry kiwi ice', titleCn: '草莓奇异果冰', shortName: 'ST' },
|
||||
|
|
@ -680,10 +680,10 @@ const flavorsData = [
|
|||
{ name: 'strawberry-watermelon', title: 'strawberry watermelon', titleCn: '草莓西瓜', shortName: 'ST' },
|
||||
{ name: 'strawberry-watermelon-ice', title: 'strawberry watermelon ice', titleCn: '草莓西瓜冰', shortName: 'ST' },
|
||||
{ name: 'strawmelon-peach', title: 'strawmelon peach', titleCn: '草莓桃', shortName: 'ST' },
|
||||
{ name: 'strawmelon-peach-(solid)', title: 'strawmelon peach (solid)', titleCn: '草莓桃(固体)', shortName: 'ST' },
|
||||
{ name: 'strawmelon-peach-(solid)', title: 'strawmelon peach (solid)', titleCn: '草莓桃(固体)', shortName: 'ST' },
|
||||
{ name: 'strawnana-orange', title: 'strawnana orange', titleCn: '草莓香蕉橙', shortName: 'ST' },
|
||||
{ name: 'summer-grape', title: 'summer grape', titleCn: '夏日葡萄', shortName: 'SU' },
|
||||
{ name: 'summer-grape-(thermal)', title: 'summer grape (thermal)', titleCn: '夏日葡萄(热感)', shortName: 'SU' },
|
||||
{ name: 'summer-grape-(thermal)', title: 'summer grape (thermal)', titleCn: '夏日葡萄(热感)', shortName: 'SU' },
|
||||
{ name: 'super-sour-blueberry-iced', title: 'super sour blueberry iced', titleCn: '超级酸蓝莓冰', shortName: 'SU' },
|
||||
{ name: 'super-spearmint', title: 'super spearmint', titleCn: '超级留兰香', shortName: 'SU' },
|
||||
{ name: 'super-spearmint-iced', title: 'super spearmint iced', titleCn: '超级留兰香冰', shortName: 'SU' },
|
||||
|
|
@ -704,7 +704,7 @@ const flavorsData = [
|
|||
{ name: 'tropical-orang-ice', title: 'tropical orang ice', titleCn: '热带橙冰', shortName: 'TR' },
|
||||
{ name: 'tropical-prism-blast', title: 'tropical prism blast', titleCn: '热带棱镜爆炸', shortName: 'TR' },
|
||||
{ name: 'tropical-splash', title: 'tropical splash', titleCn: '热带飞溅', shortName: 'TR' },
|
||||
{ name: 'tropical-splash-(solid)', title: 'tropical splash (solid)', titleCn: '热带飞溅(固体)', shortName: 'TR' },
|
||||
{ name: 'tropical-splash-(solid)', title: 'tropical splash (solid)', titleCn: '热带飞溅(固体)', shortName: 'TR' },
|
||||
{ name: 'tropical-storm-ice', title: 'tropical storm ice', titleCn: '热带风暴冰', shortName: 'TR' },
|
||||
{ name: 'tropical-summer', title: 'tropical summer', titleCn: '热带夏日', shortName: 'TR' },
|
||||
{ name: 'tropika', title: 'tropika', titleCn: '热带果', shortName: 'TR' },
|
||||
|
|
@ -728,7 +728,7 @@ const flavorsData = [
|
|||
{ name: 'watermelon-cantaloupe-honeydew-ice', title: 'watermelon cantaloupe honeydew ice', titleCn: '西瓜香瓜蜜瓜冰', shortName: 'WA' },
|
||||
{ name: 'watermelon-g', title: 'watermelon g', titleCn: '西瓜 G', shortName: 'WA' },
|
||||
{ name: 'watermelon-ice', title: 'watermelon ice', titleCn: '西瓜冰', shortName: 'WA' },
|
||||
{ name: 'watermelon-ice-(solid)', title: 'watermelon ice (solid)', titleCn: '西瓜冰(固体)', shortName: 'WA' },
|
||||
{ name: 'watermelon-ice-(solid)', title: 'watermelon ice (solid)', titleCn: '西瓜冰(固体)', shortName: 'WA' },
|
||||
{ name: 'watermelon-lime-ice', title: 'watermelon lime ice', titleCn: '西瓜青柠冰', shortName: 'WA' },
|
||||
{ name: 'watermelon-mango-tango', title: 'watermelon mango tango', titleCn: '西瓜芒果探戈', shortName: 'WA' },
|
||||
{ name: 'watermelona-cg', title: 'watermelona cg', titleCn: '西瓜 CG', shortName: 'WA' },
|
||||
|
|
@ -750,7 +750,7 @@ const flavorsData = [
|
|||
{ name: 'wild-strawberry-watermelon', title: 'wild strawberry watermelon', titleCn: '野生草莓西瓜', shortName: 'WI' },
|
||||
{ name: 'wild-white-grape', title: 'wild white grape', titleCn: '野生白葡萄', shortName: 'WI' },
|
||||
{ name: 'wild-white-grape-ice', title: 'wild white grape ice', titleCn: '野生白葡萄冰', shortName: 'WI' },
|
||||
{ name: 'wild-white-grape-iced', title: 'wild white grape iced', titleCn: '野生白葡萄冰(冷饮)', shortName: 'WI' },
|
||||
{ name: 'wild-white-grape-iced', title: 'wild white grape iced', titleCn: '野生白葡萄冰(冷饮)', shortName: 'WI' },
|
||||
{ name: 'winter-berry-ice', title: 'winter berry ice', titleCn: '冬季浆果冰', shortName: 'WI' },
|
||||
{ name: 'winter-green', title: 'winter green', titleCn: '冬青', shortName: 'WI' },
|
||||
{ name: 'wintergreen', title: 'wintergreen', titleCn: '冬青薄荷', shortName: 'WI' },
|
||||
|
|
|
|||
|
|
@ -23,19 +23,38 @@ export default class TemplateSeeder implements Seeder {
|
|||
const templates = [
|
||||
{
|
||||
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模板',
|
||||
testData: JSON.stringify({
|
||||
category: {
|
||||
shortName: 'CAT',
|
||||
"category": {
|
||||
"name": "nicotine-pouches",
|
||||
"shortName": "NP"
|
||||
},
|
||||
attributes: [
|
||||
{ shortName: 'BR' },
|
||||
{ shortName: 'FL' },
|
||||
{ shortName: '10MG' },
|
||||
{ shortName: 'DRY' },
|
||||
],
|
||||
}),
|
||||
"attributes": [
|
||||
{ "dict": {"name": "brand"},"shortName": "YOONE" },
|
||||
{ "dict": {"name": "flavor"},"shortName": "FL" },
|
||||
{ "dict": {"name": "strength"},"shortName": "10MG" },
|
||||
{ "dict": {"name": "humidity"},"shortName": "DRY" }
|
||||
]
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: 'product.title',
|
||||
|
|
|
|||
|
|
@ -50,13 +50,37 @@ export class UnifiedSearchParamsDTO<Where=Record<string, any>> {
|
|||
required: false,
|
||||
})
|
||||
orderBy?: Record<string, 'asc' | 'desc'> | string;
|
||||
|
||||
@ApiProperty({
|
||||
description: '分组字段,例如 "categoryId"',
|
||||
type: 'string',
|
||||
required: false,
|
||||
})
|
||||
groupBy?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Shopyy获取所有订单参数DTO
|
||||
*/
|
||||
export class ShopyyGetAllOrdersParams {
|
||||
@ApiProperty({ description: '每页数量', example: 100, required: false })
|
||||
per_page?: number;
|
||||
|
||||
@ApiProperty({ description: '支付时间范围开始', example: '2023-01-01T00:00:00Z', required: false })
|
||||
pay_at_min?: string;
|
||||
|
||||
@ApiProperty({ description: '支付时间范围结束', example: '2023-01-01T23:59:59Z', required: false })
|
||||
pay_at_max?: string;
|
||||
|
||||
@ApiProperty({ description: '排序字段', example: 'id', required: false })
|
||||
order_field?: string;//排序字段(默认id) id=订单ID updated_at=最后更新时间 pay_at=支付时间
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量操作错误项
|
||||
*/
|
||||
export interface BatchErrorItem {
|
||||
// 错误项标识(可以是ID、邮箱等)
|
||||
// 错误项标识(可以是ID、邮箱等)
|
||||
identifier: string;
|
||||
// 错误信息
|
||||
error: string;
|
||||
|
|
@ -76,7 +100,7 @@ export interface BatchOperationResult {
|
|||
updated?: number;
|
||||
// 删除数量
|
||||
deleted?: number;
|
||||
// 跳过的数量(如数据已存在或无需处理)
|
||||
// 跳过的数量(如数据已存在或无需处理)
|
||||
skipped?: number;
|
||||
// 错误列表
|
||||
errors: BatchErrorItem[];
|
||||
|
|
@ -101,7 +125,7 @@ export class SyncOperationResult implements BatchOperationResult {
|
|||
* 批量操作错误项DTO
|
||||
*/
|
||||
export class BatchErrorItemDTO {
|
||||
@ApiProperty({ description: '错误项标识(如ID、邮箱等)', type: String })
|
||||
@ApiProperty({ description: '错误项标识(如ID、邮箱等)', type: String })
|
||||
@Rule(RuleType.string().required())
|
||||
identifier: string;
|
||||
|
||||
|
|
@ -164,7 +188,7 @@ export class SyncParamsDTO {
|
|||
@Rule(RuleType.string().optional())
|
||||
endDate?: string;
|
||||
|
||||
@ApiProperty({ description: '强制同步(忽略缓存)', type: Boolean, required: false, default: false })
|
||||
@ApiProperty({ description: '强制同步(忽略缓存)', type: Boolean, required: false, default: false })
|
||||
@Rule(RuleType.boolean().optional())
|
||||
force?: boolean = false;
|
||||
}
|
||||
|
|
@ -183,7 +207,7 @@ export class BatchQueryDTO {
|
|||
}
|
||||
|
||||
/**
|
||||
* 批量操作结果类(泛型支持)
|
||||
* 批量操作结果类(泛型支持)
|
||||
*/
|
||||
export class BatchOperationResultDTOGeneric<T> extends BatchOperationResultDTO {
|
||||
@ApiProperty({ description: '操作成功的数据列表', type: Array })
|
||||
|
|
@ -191,7 +215,7 @@ export class BatchOperationResultDTOGeneric<T> extends BatchOperationResultDTO {
|
|||
}
|
||||
|
||||
/**
|
||||
* 同步操作结果类(泛型支持)
|
||||
* 同步操作结果类(泛型支持)
|
||||
*/
|
||||
export class SyncOperationResultDTOGeneric<T> extends SyncOperationResultDTO {
|
||||
@ApiProperty({ description: '同步成功的数据列表', type: Array })
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import { Rule, RuleType } from '@midwayjs/validate';
|
|||
* 批量操作错误项
|
||||
*/
|
||||
export interface BatchErrorItem {
|
||||
// 错误项标识(可以是ID、邮箱等)
|
||||
// 错误项标识(可以是ID、邮箱等)
|
||||
identifier: string;
|
||||
// 错误信息
|
||||
error: string;
|
||||
|
|
@ -25,7 +25,7 @@ export interface BatchOperationResult {
|
|||
updated?: number;
|
||||
// 删除数量
|
||||
deleted?: number;
|
||||
// 跳过的数量(如数据已存在或无需处理)
|
||||
// 跳过的数量(如数据已存在或无需处理)
|
||||
skipped?: number;
|
||||
// 错误列表
|
||||
errors: BatchErrorItem[];
|
||||
|
|
@ -43,7 +43,7 @@ export interface SyncOperationResult extends BatchOperationResult {
|
|||
* 批量操作错误项DTO
|
||||
*/
|
||||
export class BatchErrorItemDTO {
|
||||
@ApiProperty({ description: '错误项标识(如ID、邮箱等)', type: String })
|
||||
@ApiProperty({ description: '错误项标识(如ID、邮箱等)', type: String })
|
||||
@Rule(RuleType.string().required())
|
||||
identifier: string;
|
||||
|
||||
|
|
@ -114,7 +114,7 @@ export class BatchDeleteDTO {
|
|||
}
|
||||
|
||||
/**
|
||||
* 批量操作请求DTO(包含增删改)
|
||||
* 批量操作请求DTO(包含增删改)
|
||||
*/
|
||||
export class BatchOperationDTO<T = any> {
|
||||
@ApiProperty({ description: '要创建的数据列表', type: Array, required: false })
|
||||
|
|
@ -175,7 +175,7 @@ export class SyncParamsDTO {
|
|||
@Rule(RuleType.string().optional())
|
||||
endDate?: string;
|
||||
|
||||
@ApiProperty({ description: '强制同步(忽略缓存)', type: Boolean, required: false, default: false })
|
||||
@ApiProperty({ description: '强制同步(忽略缓存)', type: Boolean, required: false, default: false })
|
||||
@Rule(RuleType.boolean().optional())
|
||||
force?: boolean = false;
|
||||
}
|
||||
|
|
@ -194,7 +194,7 @@ export class BatchQueryDTO {
|
|||
}
|
||||
|
||||
/**
|
||||
* 批量操作结果类(泛型支持)
|
||||
* 批量操作结果类(泛型支持)
|
||||
*/
|
||||
export class BatchOperationResultDTOGeneric<T> extends BatchOperationResultDTO {
|
||||
@ApiProperty({ description: '操作成功的数据列表', type: Array })
|
||||
|
|
@ -202,7 +202,7 @@ export class BatchOperationResultDTOGeneric<T> extends BatchOperationResultDTO {
|
|||
}
|
||||
|
||||
/**
|
||||
* 同步操作结果类(泛型支持)
|
||||
* 同步操作结果类(泛型支持)
|
||||
*/
|
||||
export class SyncOperationResultDTOGeneric<T> extends SyncOperationResultDTO {
|
||||
@ApiProperty({ description: '同步成功的数据列表', type: Array })
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { ApiProperty } from '@midwayjs/swagger';
|
|||
import { UnifiedSearchParamsDTO } from './api.dto';
|
||||
import { Customer } from '../entity/customer.entity';
|
||||
|
||||
// 客户基本信息DTO(用于响应)
|
||||
// 客户基本信息DTO(用于响应)
|
||||
export class CustomerDTO extends Customer{
|
||||
@ApiProperty({ description: '客户ID' })
|
||||
id: number;
|
||||
|
|
@ -163,11 +163,11 @@ export class UpdateCustomerDTO {
|
|||
tags?: string[];
|
||||
}
|
||||
|
||||
// 查询单个客户响应DTO(继承基本信息)
|
||||
// 查询单个客户响应DTO(继承基本信息)
|
||||
export class GetCustomerDTO extends CustomerDTO {
|
||||
// 可以添加额外的详细信息字段
|
||||
}
|
||||
// 客户统计信息DTO(包含订单统计)
|
||||
// 客户统计信息DTO(包含订单统计)
|
||||
export class CustomerStatisticDTO extends CustomerDTO {
|
||||
@ApiProperty({ description: '创建日期' })
|
||||
date_created: Date;
|
||||
|
|
@ -209,7 +209,7 @@ export class CustomerStatisticWhereDTO {
|
|||
customerId?: number;
|
||||
}
|
||||
|
||||
// 客户统计查询参数DTO(继承通用查询参数)
|
||||
// 客户统计查询参数DTO(继承通用查询参数)
|
||||
export type CustomerStatisticQueryParamsDTO = UnifiedSearchParamsDTO<CustomerStatisticWhereDTO>;
|
||||
|
||||
// 客户统计列表响应DTO
|
||||
|
|
@ -259,7 +259,7 @@ export class BatchDeleteCustomerDTO {
|
|||
|
||||
// ====================== 查询操作 ======================
|
||||
|
||||
// 客户查询条件DTO(用于UnifiedSearchParamsDTO的where参数)
|
||||
// 客户查询条件DTO(用于UnifiedSearchParamsDTO的where参数)
|
||||
export class CustomerWhereDTO {
|
||||
@ApiProperty({ description: '邮箱筛选', required: false })
|
||||
email?: string;
|
||||
|
|
@ -284,10 +284,10 @@ export class CustomerWhereDTO {
|
|||
role?: string;
|
||||
}
|
||||
|
||||
// 客户查询参数DTO(继承通用查询参数)
|
||||
// 客户查询参数DTO(继承通用查询参数)
|
||||
export type CustomerQueryParamsDTO = UnifiedSearchParamsDTO<CustomerWhereDTO>;
|
||||
|
||||
// 客户列表响应DTO(参考site-api.dto.ts中的分页格式)
|
||||
// 客户列表响应DTO(参考site-api.dto.ts中的分页格式)
|
||||
export class CustomerListResponseDTO {
|
||||
@ApiProperty({ description: '客户列表', type: [CustomerDTO] })
|
||||
items: CustomerDTO[];
|
||||
|
|
@ -359,6 +359,6 @@ export class SyncCustomersDTO {
|
|||
@ApiProperty({ description: '站点ID' })
|
||||
siteId: number;
|
||||
|
||||
@ApiProperty({ description: '查询参数(支持where和orderBy)', type: UnifiedSearchParamsDTO, required: false })
|
||||
@ApiProperty({ description: '查询参数(支持where和orderBy)', type: UnifiedSearchParamsDTO, required: false })
|
||||
params?: UnifiedSearchParamsDTO<CustomerWhereDTO>;
|
||||
}
|
||||
|
|
@ -19,15 +19,22 @@ export class ShipmentBookDTO {
|
|||
@ApiProperty({ type: 'number', isArray: true })
|
||||
@Rule(RuleType.array<number>().default([]))
|
||||
orderIds?: number[];
|
||||
|
||||
@ApiProperty()
|
||||
@Rule(RuleType.string())
|
||||
shipmentPlatform: string;
|
||||
}
|
||||
|
||||
export class ShipmentFeeBookDTO {
|
||||
|
||||
@ApiProperty()
|
||||
shipmentPlatform: string;
|
||||
@ApiProperty()
|
||||
stockPointId: number;
|
||||
@ApiProperty()
|
||||
sender: string;
|
||||
@ApiProperty()
|
||||
startPhone: string;
|
||||
startPhone: string|any;
|
||||
@ApiProperty()
|
||||
startPostalCode: string;
|
||||
@ApiProperty()
|
||||
|
|
@ -63,6 +70,8 @@ export class ShipmentFeeBookDTO {
|
|||
weight: number;
|
||||
@ApiProperty()
|
||||
weightUom: string;
|
||||
@ApiProperty()
|
||||
address_id: number;
|
||||
}
|
||||
|
||||
export class PaymentMethodDTO {
|
||||
|
|
|
|||
|
|
@ -98,13 +98,9 @@ export class QueryOrderDTO {
|
|||
}
|
||||
|
||||
export class QueryOrderSalesDTO {
|
||||
@ApiProperty()
|
||||
@ApiProperty({ description: '是否为原产品还是库存产品' })
|
||||
@Rule(RuleType.bool().default(false))
|
||||
isSource: boolean;
|
||||
|
||||
@ApiProperty()
|
||||
@Rule(RuleType.bool().default(false))
|
||||
exceptPackage: boolean;
|
||||
isSource: boolean;
|
||||
|
||||
@ApiProperty({ example: '1', description: '页码' })
|
||||
@Rule(RuleType.number())
|
||||
|
|
@ -114,19 +110,31 @@ export class QueryOrderSalesDTO {
|
|||
@Rule(RuleType.number())
|
||||
pageSize: number;
|
||||
|
||||
@ApiProperty()
|
||||
@ApiProperty({ description: '排序对象,格式如 { productName: "asc", sku: "desc" }',type: 'any', required: false })
|
||||
@Rule(RuleType.object().allow(null))
|
||||
orderBy?: Record<string, 'asc' | 'desc'>;
|
||||
// filter
|
||||
@ApiProperty({ description: '是否排除套餐' })
|
||||
@Rule(RuleType.bool().default(false))
|
||||
exceptPackage: boolean;
|
||||
|
||||
@ApiProperty({ description: '站点ID' })
|
||||
@Rule(RuleType.number())
|
||||
siteId: number;
|
||||
|
||||
@ApiProperty()
|
||||
@ApiProperty({ description: '名称' })
|
||||
@Rule(RuleType.string())
|
||||
name: string;
|
||||
|
||||
@ApiProperty()
|
||||
@ApiProperty({ description: 'SKU' })
|
||||
@Rule(RuleType.string())
|
||||
sku: string;
|
||||
|
||||
@ApiProperty({ description: '开始日期' })
|
||||
@Rule(RuleType.date())
|
||||
startDate: Date;
|
||||
|
||||
@ApiProperty()
|
||||
@ApiProperty({ description: '结束日期' })
|
||||
@Rule(RuleType.date())
|
||||
endDate: Date;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -59,6 +59,10 @@ export class CreateProductDTO {
|
|||
@Rule(RuleType.number())
|
||||
categoryId?: number;
|
||||
|
||||
@ApiProperty({ description: '分类名称', required: false })
|
||||
@Rule(RuleType.string().optional())
|
||||
categoryName?: string;
|
||||
|
||||
@ApiProperty({ description: '站点 SKU 列表', type: 'array', required: false })
|
||||
@Rule(RuleType.array().items(RuleType.string()).optional())
|
||||
siteSkus?: string[];
|
||||
|
|
@ -86,7 +90,10 @@ export class CreateProductDTO {
|
|||
@Rule(RuleType.number())
|
||||
promotionPrice?: number;
|
||||
|
||||
|
||||
// 产品图片URL
|
||||
@ApiProperty({ description: '产品图片URL', example: 'https://example.com/image.jpg', required: false })
|
||||
@Rule(RuleType.string().optional())
|
||||
image?: string;
|
||||
|
||||
// 商品类型(默认 single; bundle 需手动设置组成)
|
||||
@ApiProperty({ description: '商品类型', enum: ['single', 'bundle'], default: 'single', required: false })
|
||||
|
|
@ -139,6 +146,10 @@ export class UpdateProductDTO {
|
|||
@Rule(RuleType.number())
|
||||
categoryId?: number;
|
||||
|
||||
@ApiProperty({ description: '分类名称', required: false })
|
||||
@Rule(RuleType.string().optional())
|
||||
categoryName?: string;
|
||||
|
||||
@ApiProperty({ description: '站点 SKU 列表', type: 'array', required: false })
|
||||
@Rule(RuleType.array().items(RuleType.string()).optional())
|
||||
siteSkus?: string[];
|
||||
|
|
@ -153,7 +164,10 @@ export class UpdateProductDTO {
|
|||
@Rule(RuleType.number())
|
||||
promotionPrice?: number;
|
||||
|
||||
|
||||
// 产品图片URL
|
||||
@ApiProperty({ description: '产品图片URL', example: 'https://example.com/image.jpg', required: false })
|
||||
@Rule(RuleType.string().optional())
|
||||
image?: string;
|
||||
|
||||
// 属性更新(可选, 支持增量替换指定字典的属性项)
|
||||
@ApiProperty({ description: '属性列表', type: 'array', required: false })
|
||||
|
|
@ -228,6 +242,10 @@ export class BatchUpdateProductDTO {
|
|||
@Rule(RuleType.number().optional())
|
||||
promotionPrice?: number;
|
||||
|
||||
@ApiProperty({ description: '产品图片URL', example: 'https://example.com/image.jpg', required: false })
|
||||
@Rule(RuleType.string().optional())
|
||||
image?: string;
|
||||
|
||||
@ApiProperty({ description: '属性列表', type: 'array', required: false })
|
||||
@Rule(RuleType.array().optional())
|
||||
attributes?: AttributeInputDTO[];
|
||||
|
|
@ -301,6 +319,8 @@ export interface ProductWhereFilter {
|
|||
updatedAtStart?: string;
|
||||
// 更新时间范围结束
|
||||
updatedAtEnd?: string;
|
||||
// TODO 使用 attributes 过滤
|
||||
attributes?: Record<string, string>;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -2,6 +2,8 @@ import { ApiProperty } from '@midwayjs/swagger';
|
|||
import {
|
||||
UnifiedPaginationDTO,
|
||||
} from './api.dto';
|
||||
import { Dict } from '../entity/dict.entity';
|
||||
import { Product } from '../entity/product.entity';
|
||||
// export class UnifiedOrderWhere{
|
||||
// []
|
||||
// }
|
||||
|
|
@ -17,6 +19,29 @@ export enum OrderFulfillmentStatus {
|
|||
// 确认发货
|
||||
CONFIRMED,
|
||||
}
|
||||
export enum OrderPaymentStatus {
|
||||
// 待支付
|
||||
PENDING,
|
||||
// 支付中
|
||||
PAYING,
|
||||
// 部分支付
|
||||
PARTIALLY_PAID,
|
||||
// 已支付
|
||||
PAID,
|
||||
// 支付失败
|
||||
FAILED,
|
||||
// 部分退款
|
||||
PARTIALLY_REFUNDED,
|
||||
// 已退款
|
||||
REFUNDED,
|
||||
// 已取消
|
||||
CANCELLED,
|
||||
}
|
||||
//
|
||||
export class UnifiedProductWhere {
|
||||
sku?: string;
|
||||
[prop:string]:any
|
||||
}
|
||||
export class UnifiedTagDTO {
|
||||
// 标签DTO用于承载统一标签数据
|
||||
@ApiProperty({ description: '标签ID' })
|
||||
|
|
@ -135,8 +160,10 @@ export class UnifiedProductAttributeDTO {
|
|||
@ApiProperty({ description: '属性选项', type: [String] })
|
||||
options: string[];
|
||||
|
||||
@ApiProperty({ description: '变体属性值(单个值)', required: false })
|
||||
@ApiProperty({ description: '变体属性值(单个值)', required: false })
|
||||
option?: string;
|
||||
// 这个是属性的父级字典项
|
||||
dict?: Dict;
|
||||
}
|
||||
|
||||
export class UnifiedProductVariationDTO {
|
||||
|
|
@ -280,17 +307,7 @@ export class UnifiedProductDTO {
|
|||
type: 'object',
|
||||
required: false,
|
||||
})
|
||||
erpProduct?: {
|
||||
id: number;
|
||||
sku: string;
|
||||
name: string;
|
||||
nameCn?: string;
|
||||
category?: any;
|
||||
attributes?: any[];
|
||||
components?: any[];
|
||||
price: number;
|
||||
promotionPrice: number;
|
||||
};
|
||||
erpProduct?: Product
|
||||
}
|
||||
|
||||
export class UnifiedOrderRefundDTO {
|
||||
|
|
@ -782,17 +799,20 @@ export class UpdateWebhookDTO {
|
|||
|
||||
|
||||
export class FulfillmentItemDTO {
|
||||
@ApiProperty({ description: '订单项ID' })
|
||||
@ApiProperty({ description: '订单项ID' ,required: false})
|
||||
order_item_id: number;
|
||||
|
||||
@ApiProperty({ description: '数量' })
|
||||
@ApiProperty({ description: '数量' ,required:false})
|
||||
quantity: number;
|
||||
}
|
||||
|
||||
export class FulfillmentDTO {
|
||||
@ApiProperty({ description: '物流id', required: false })
|
||||
tracking_id?: string;
|
||||
@ApiProperty({ description: '物流单号', required: false })
|
||||
tracking_number?: string;
|
||||
|
||||
@ApiProperty({ description: "物流产品代码" , required: false})
|
||||
tracking_product_code?: string;
|
||||
@ApiProperty({ description: '物流公司', required: false })
|
||||
shipping_provider?: string;
|
||||
|
||||
|
|
|
|||
|
|
@ -121,7 +121,7 @@ export class UpdateSiteDTO {
|
|||
skuPrefix?: string;
|
||||
|
||||
// 区域
|
||||
@ApiProperty({ description: '区域' })
|
||||
@ApiProperty({ description: '区域', required: false })
|
||||
@Rule(RuleType.array().items(RuleType.string()).optional())
|
||||
areas?: string[];
|
||||
|
||||
|
|
@ -133,6 +133,10 @@ export class UpdateSiteDTO {
|
|||
@ApiProperty({ description: '站点网站URL', required: false })
|
||||
@Rule(RuleType.string().optional())
|
||||
websiteUrl?: string;
|
||||
|
||||
@ApiProperty({ description: 'Webhook URL', required: false })
|
||||
@Rule(RuleType.string().optional())
|
||||
webhookUrl?: string;
|
||||
}
|
||||
|
||||
export class QuerySiteDTO {
|
||||
|
|
@ -152,7 +156,7 @@ export class QuerySiteDTO {
|
|||
@Rule(RuleType.boolean().optional())
|
||||
isDisabled?: boolean;
|
||||
|
||||
@ApiProperty({ description: '站点ID列表(逗号分隔)', required: false })
|
||||
@ApiProperty({ description: '站点ID列表(逗号分隔)', required: false })
|
||||
@Rule(RuleType.string().optional())
|
||||
ids?: string;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,6 +19,10 @@ export class OrderStatisticsParams {
|
|||
@Rule(RuleType.number().allow(null))
|
||||
siteId?: number;
|
||||
|
||||
@ApiProperty()
|
||||
@Rule(RuleType.array().allow(null))
|
||||
country?: any[];
|
||||
|
||||
@ApiProperty({
|
||||
enum: ['all', 'first_purchase', 'repeat_purchase'],
|
||||
default: 'all',
|
||||
|
|
|
|||
|
|
@ -8,11 +8,11 @@ export interface WooProduct {
|
|||
id: number;
|
||||
// 创建时间
|
||||
date_created: string;
|
||||
// 创建时间(GMT)
|
||||
// 创建时间(GMT)
|
||||
date_created_gmt: string;
|
||||
// 更新时间
|
||||
date_modified: string;
|
||||
// 更新时间(GMT)
|
||||
// 更新时间(GMT)
|
||||
date_modified_gmt: string;
|
||||
// 产品类型 simple grouped external variable
|
||||
type: string;
|
||||
|
|
@ -20,7 +20,7 @@ export interface WooProduct {
|
|||
status: string;
|
||||
// 是否为特色产品
|
||||
featured: boolean;
|
||||
// 目录可见性选项:visible, catalog, search and hidden. Default is visible.
|
||||
// 目录可见性选项:visible, catalog, search and hidden. Default is visible.
|
||||
catalog_visibility: string;
|
||||
|
||||
// 常规价格
|
||||
|
|
@ -117,9 +117,9 @@ export interface WooProduct {
|
|||
// 购买备注
|
||||
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;
|
||||
// 元数据
|
||||
|
|
@ -130,11 +130,11 @@ export interface WooVariation {
|
|||
id: number;
|
||||
// 创建时间
|
||||
date_created: string;
|
||||
// 创建时间(GMT)
|
||||
// 创建时间(GMT)
|
||||
date_created_gmt: string;
|
||||
// 更新时间
|
||||
date_modified: string;
|
||||
// 更新时间(GMT)
|
||||
// 更新时间(GMT)
|
||||
date_modified_gmt: string;
|
||||
// 变体描述
|
||||
description: string;
|
||||
|
|
@ -150,11 +150,11 @@ export interface WooVariation {
|
|||
price_html?: string;
|
||||
// 促销开始日期
|
||||
date_on_sale_from?: string;
|
||||
// 促销开始日期(GMT)
|
||||
// 促销开始日期(GMT)
|
||||
date_on_sale_from_gmt?: string;
|
||||
// 促销结束日期
|
||||
date_on_sale_to?: string;
|
||||
// 促销结束日期(GMT)
|
||||
// 促销结束日期(GMT)
|
||||
date_on_sale_to_gmt?: string;
|
||||
// 是否在促销中
|
||||
on_sale: boolean;
|
||||
|
|
@ -369,18 +369,35 @@ export interface WooOrder {
|
|||
date_created_gmt?: string;
|
||||
date_modified?: string;
|
||||
date_modified_gmt?: string;
|
||||
// 物流追踪信息
|
||||
fulfillments?: Array<{
|
||||
tracking_number?: string;
|
||||
shipping_provider?: string;
|
||||
shipping_method?: string;
|
||||
status?: string;
|
||||
date_created?: string;
|
||||
items?: Array<{
|
||||
order_item_id?: number;
|
||||
quantity?: number;
|
||||
}>;
|
||||
}>;
|
||||
}
|
||||
export interface MetaDataFulfillment {
|
||||
custom_tracking_link: string;
|
||||
custom_tracking_provider: string;
|
||||
date_shipped: number;
|
||||
source: string;
|
||||
status_shipped: string;
|
||||
tracking_id: string;
|
||||
tracking_number: string;
|
||||
tracking_product_code: string;
|
||||
tracking_provider: string;
|
||||
user_id: number;
|
||||
}
|
||||
// 这个是一个插件的物流追踪信息
|
||||
// 接口:
|
||||
export interface WooFulfillment {
|
||||
data_sipped: string;
|
||||
tracking_id: string;
|
||||
tracking_link: string;
|
||||
tracking_number: string;
|
||||
tracking_provider: string;
|
||||
}
|
||||
// https://docs.zorem.com/docs/ast-free/developers/adding-tracking-info-to-orders/
|
||||
export interface WooFulfillmentCreateParams {
|
||||
order_id: string;
|
||||
tracking_provider: string;
|
||||
tracking_number: string;
|
||||
date_shipped?: string;
|
||||
status_shipped?: string;
|
||||
}
|
||||
export interface WooOrderRefund {
|
||||
id?: number;
|
||||
|
|
@ -552,7 +569,8 @@ export interface WooOrderSearchParams {
|
|||
order: string;
|
||||
orderby: string;
|
||||
parant: string[];
|
||||
status: (WooOrderStatusSearchParams)[];
|
||||
parent_exclude: string[];
|
||||
status: WooOrderStatusSearchParams[];
|
||||
customer: number;
|
||||
product: number;
|
||||
dp: number;
|
||||
|
|
@ -616,6 +634,83 @@ export interface ListParams {
|
|||
parant: string[];
|
||||
parent_exclude: string[];
|
||||
}
|
||||
export interface WpMediaGetListParams {
|
||||
// 请求范围,决定响应中包含的字段
|
||||
// 默认: view
|
||||
// 可选值: view, embed, edit
|
||||
context?: 'view' | 'embed' | 'edit';
|
||||
|
||||
// 当前页码
|
||||
// 默认: 1
|
||||
page?: number;
|
||||
|
||||
// 每页最大返回数量
|
||||
// 默认: 10
|
||||
per_page?: number;
|
||||
|
||||
// 搜索字符串,限制结果匹配
|
||||
search?: string;
|
||||
|
||||
// ISO8601格式日期,限制发布时间之后的结果
|
||||
after?: string;
|
||||
|
||||
// ISO8601格式日期,限制修改时间之后的结果
|
||||
modified_after?: string;
|
||||
|
||||
// 作者ID数组,限制结果集为特定作者
|
||||
author?: number[];
|
||||
|
||||
// 作者ID数组,排除特定作者的结果
|
||||
author_exclude?: number[];
|
||||
|
||||
// ISO8601格式日期,限制发布时间之前的结果
|
||||
before?: string;
|
||||
|
||||
// ISO8601格式日期,限制修改时间之前的结果
|
||||
modified_before?: string;
|
||||
|
||||
// ID数组,排除特定ID的结果
|
||||
exclude?: number[];
|
||||
|
||||
// ID数组,限制结果集为特定ID
|
||||
include?: number[];
|
||||
|
||||
// 结果集偏移量
|
||||
offset?: number;
|
||||
|
||||
// 排序方向
|
||||
// 默认: desc
|
||||
// 可选值: asc, desc
|
||||
order?: 'asc' | 'desc';
|
||||
|
||||
// 排序字段
|
||||
// 默认: date
|
||||
// 可选值: author, date, id, include, modified, parent, relevance, slug, include_slugs, title
|
||||
orderby?: 'author' | 'date' | 'id' | 'include' | 'modified' | 'parent' | 'relevance' | 'slug' | 'include_slugs' | 'title';
|
||||
|
||||
// 父ID数组,限制结果集为特定父ID
|
||||
parent?: number[];
|
||||
|
||||
// 父ID数组,排除特定父ID的结果
|
||||
parent_exclude?: number[];
|
||||
|
||||
// 搜索的列名数组
|
||||
search_columns?: string[];
|
||||
|
||||
// slug数组,限制结果集为特定slug
|
||||
slug?: string[];
|
||||
|
||||
// 状态数组,限制结果集为特定状态
|
||||
// 默认: inherit
|
||||
status?: string[];
|
||||
|
||||
// 媒体类型,限制结果集为特定媒体类型
|
||||
// 可选值: image, video, text, application, audio
|
||||
media_type?: 'image' | 'video' | 'text' | 'application' | 'audio';
|
||||
|
||||
// MIME类型,限制结果集为特定MIME类型
|
||||
mime_type?: string;
|
||||
}
|
||||
export enum WooContext {
|
||||
view,
|
||||
edit
|
||||
|
|
|
|||
|
|
@ -29,6 +29,10 @@ export class Dict {
|
|||
@OneToMany(() => DictItem, item => item.dict)
|
||||
items: DictItem[];
|
||||
|
||||
// 排序
|
||||
@Column({ default: 0, comment: '排序' })
|
||||
sort: number;
|
||||
|
||||
// 是否可删除
|
||||
@Column({ default: true, comment: '是否可删除' })
|
||||
deletable: boolean;
|
||||
|
|
|
|||
|
|
@ -106,7 +106,7 @@ export class Order {
|
|||
@Expose()
|
||||
cart_tax: number;
|
||||
|
||||
@ApiProperty()
|
||||
@ApiProperty({ type: "总金额"})
|
||||
@Column('decimal', { precision: 10, scale: 2, default: 0 })
|
||||
@Expose()
|
||||
total: number;
|
||||
|
|
@ -272,6 +272,14 @@ export class Order {
|
|||
@Expose()
|
||||
updatedAt: Date;
|
||||
|
||||
@ApiProperty({ type: 'json', nullable: true, description: '订单项列表' })
|
||||
@Expose()
|
||||
orderItems?: any[];
|
||||
|
||||
@ApiProperty({ type: 'json', nullable: true, description: '销售项列表' })
|
||||
@Expose()
|
||||
orderSales?: any[];
|
||||
|
||||
// 在插入或更新前处理用户代理字符串
|
||||
@BeforeInsert()
|
||||
@BeforeUpdate()
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import { ApiProperty } from '@midwayjs/swagger';
|
||||
import { Exclude, Expose } from 'class-transformer';
|
||||
import {
|
||||
BeforeInsert,
|
||||
BeforeUpdate,
|
||||
// BeforeInsert,
|
||||
// BeforeUpdate,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
Entity,
|
||||
|
|
@ -22,26 +22,31 @@ export class OrderSale {
|
|||
@Expose()
|
||||
id?: number;
|
||||
|
||||
@ApiProperty()
|
||||
@ApiProperty({ name:'原始订单ID' })
|
||||
@Column()
|
||||
@Expose()
|
||||
orderId: number; // 订单 ID
|
||||
|
||||
@ApiProperty()
|
||||
@Column({ nullable: true })
|
||||
@ApiProperty({ name:'站点' })
|
||||
@Column()
|
||||
@Expose()
|
||||
siteId: number; // 来源站点唯一标识
|
||||
|
||||
@ApiProperty()
|
||||
@ApiProperty({name: "原始订单 itemId"})
|
||||
@Column({ nullable: true })
|
||||
@Expose()
|
||||
externalOrderItemId: string; // WooCommerce 订单item ID
|
||||
|
||||
@ApiProperty()
|
||||
@ApiProperty({name: "父产品 ID"})
|
||||
@Column({ nullable: true })
|
||||
@Expose()
|
||||
parentProductId?: number; // 父产品 ID 用于统计套餐 如果是单品则不记录
|
||||
|
||||
@ApiProperty({name: "产品 ID"})
|
||||
@Column()
|
||||
@Expose()
|
||||
productId: number;
|
||||
|
||||
|
||||
@ApiProperty()
|
||||
@Column()
|
||||
@Expose()
|
||||
|
|
@ -50,7 +55,7 @@ export class OrderSale {
|
|||
@ApiProperty({ description: 'sku', type: 'string' })
|
||||
@Expose()
|
||||
@Column()
|
||||
sku: string;
|
||||
sku: string;// 库存产品sku
|
||||
|
||||
@ApiProperty()
|
||||
@Column()
|
||||
|
|
@ -62,25 +67,40 @@ export class OrderSale {
|
|||
@Expose()
|
||||
isPackage: boolean;
|
||||
|
||||
@ApiProperty()
|
||||
@Column({ default: false })
|
||||
@ApiProperty({ description: '商品品类', type: 'string',nullable: true})
|
||||
@Expose()
|
||||
isYoone: boolean;
|
||||
@Column({ nullable: true })
|
||||
category?: string;
|
||||
// TODO 这个其实还是直接保存 product 比较好
|
||||
@ApiProperty({ description: '品牌', type: 'string',nullable: true})
|
||||
@Expose()
|
||||
@Column({ nullable: true })
|
||||
brand?: string;
|
||||
|
||||
@ApiProperty()
|
||||
@Column({ default: false })
|
||||
@ApiProperty({ description: '口味', type: 'string', nullable: true })
|
||||
@Expose()
|
||||
isZex: boolean;
|
||||
@Column({ nullable: true })
|
||||
flavor?: string;
|
||||
|
||||
@ApiProperty({ nullable: true })
|
||||
@Column({ type: 'int', nullable: true })
|
||||
@ApiProperty({ description: '湿度', type: 'string', nullable: true })
|
||||
@Expose()
|
||||
size: number | null;
|
||||
@Column({ nullable: true })
|
||||
humidity?: string;
|
||||
|
||||
@ApiProperty()
|
||||
@Column({ default: false })
|
||||
@ApiProperty({ description: '尺寸', type: 'string', nullable: true })
|
||||
@Expose()
|
||||
isYooneNew: boolean;
|
||||
@Column({ nullable: true })
|
||||
size?: string;
|
||||
|
||||
@ApiProperty({name: '强度', nullable: true })
|
||||
@Column({ nullable: true })
|
||||
@Expose()
|
||||
strength: string | null;
|
||||
|
||||
@ApiProperty({ description: '版本', type: 'string', nullable: true })
|
||||
@Expose()
|
||||
@Column({ nullable: true })
|
||||
version?: string;
|
||||
|
||||
@ApiProperty({
|
||||
example: '2022-12-12 11:11:11',
|
||||
|
|
@ -97,25 +117,4 @@ export class OrderSale {
|
|||
@UpdateDateColumn()
|
||||
@Expose()
|
||||
updatedAt?: Date;
|
||||
|
||||
// === 自动计算逻辑 ===
|
||||
@BeforeInsert()
|
||||
@BeforeUpdate()
|
||||
setFlags() {
|
||||
if (!this.name) return;
|
||||
const lower = this.name.toLowerCase();
|
||||
this.isYoone = lower.includes('yoone');
|
||||
this.isZex = lower.includes('zex');
|
||||
this.isYooneNew = this.isYoone && lower.includes('new');
|
||||
let size: number | null = null;
|
||||
const sizes = [3, 6, 9, 12, 15, 18];
|
||||
for (const s of sizes) {
|
||||
if (lower.includes(s.toString())) {
|
||||
size = s;
|
||||
break;
|
||||
}
|
||||
}
|
||||
this.size = size;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -55,6 +55,9 @@ export class Product {
|
|||
@Column({ nullable: true })
|
||||
description?: string;
|
||||
|
||||
@ApiProperty({ example: '图片URL', description: '产品图片URL' })
|
||||
@Column({ nullable: true })
|
||||
image?: string;
|
||||
// 商品价格
|
||||
@ApiProperty({ description: '价格', example: 99.99 })
|
||||
@Column({ type: 'decimal', precision: 10, scale: 2, default: 0 })
|
||||
|
|
@ -65,14 +68,15 @@ export class Product {
|
|||
@Column({ type: 'decimal', precision: 10, scale: 2, default: 0 })
|
||||
promotionPrice: number;
|
||||
|
||||
|
||||
|
||||
|
||||
// 分类关联
|
||||
@ManyToOne(() => Category, category => category.products)
|
||||
@JoinColumn({ name: 'categoryId' })
|
||||
category: Category;
|
||||
|
||||
@ApiProperty({ description: '分类 ID', nullable: true, example: 1 })
|
||||
@Column({ nullable: true })
|
||||
categoryId?: number;
|
||||
|
||||
@ManyToMany(() => DictItem, dictItem => dictItem.products, {
|
||||
cascade: true,
|
||||
})
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ export class ProductStockComponent {
|
|||
@ApiProperty({ description: '组件所关联的 SKU', type: 'string' })
|
||||
@Column({ type: 'varchar', length: 64 })
|
||||
sku: string;
|
||||
|
||||
|
||||
@ApiProperty({ type: Number, description: '组成数量' })
|
||||
@Column({ type: 'int', default: 1 })
|
||||
quantity: number;
|
||||
|
|
|
|||
|
|
@ -54,9 +54,9 @@ export class Shipment {
|
|||
tracking_provider?: string;
|
||||
|
||||
@ApiProperty()
|
||||
@Column()
|
||||
@Column({ nullable: true })
|
||||
@Expose()
|
||||
unique_id: string;
|
||||
unique_id?: string;
|
||||
|
||||
@Column({ nullable: true })
|
||||
@Expose()
|
||||
|
|
|
|||
|
|
@ -47,6 +47,11 @@ export class ShippingAddress {
|
|||
@Expose()
|
||||
phone_number_country: string;
|
||||
|
||||
@ApiProperty()
|
||||
@Column()
|
||||
@Expose()
|
||||
email: string;
|
||||
|
||||
@ApiProperty({
|
||||
example: '2022-12-12 11:11:11',
|
||||
description: '创建时间',
|
||||
|
|
|
|||
|
|
@ -0,0 +1,86 @@
|
|||
import {
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
UpdateDateColumn,
|
||||
Entity,
|
||||
ManyToOne,
|
||||
JoinColumn,
|
||||
} from 'typeorm';
|
||||
import { ApiProperty } from '@midwayjs/swagger';
|
||||
import { Site } from './site.entity';
|
||||
import { Product } from './product.entity';
|
||||
|
||||
@Entity('site_product')
|
||||
export class SiteProduct {
|
||||
@ApiProperty({
|
||||
example: '12345',
|
||||
description: '站点商品ID',
|
||||
type: 'string',
|
||||
required: true,
|
||||
})
|
||||
@Column({ primary: true })
|
||||
id: string;
|
||||
|
||||
@ApiProperty({ description: '站点ID' })
|
||||
@Column()
|
||||
siteId: number;
|
||||
|
||||
@ApiProperty({ description: '商品ID' })
|
||||
@Column({ nullable: true })
|
||||
productId: number;
|
||||
|
||||
@ApiProperty({ description: 'sku'})
|
||||
@Column()
|
||||
sku: string;
|
||||
|
||||
@ApiProperty({ description: '类型' })
|
||||
@Column({ length: 16, default: 'single' })
|
||||
type: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: '产品名称',
|
||||
type: 'string',
|
||||
required: true,
|
||||
})
|
||||
@Column()
|
||||
name: string;
|
||||
|
||||
@ApiProperty({ description: '产品图片' })
|
||||
@Column({ default: '' })
|
||||
image: string;
|
||||
|
||||
@ApiProperty({ description: '父商品ID', example: '12345' })
|
||||
@Column({ nullable: true })
|
||||
parentId: string;
|
||||
|
||||
// 站点关联
|
||||
@ManyToOne(() => Site, site => site.id)
|
||||
@JoinColumn({ name: 'siteId' })
|
||||
site: Site;
|
||||
|
||||
// 商品关联
|
||||
@ManyToOne(() => Product, product => product.id)
|
||||
@JoinColumn({ name: 'productId' })
|
||||
product: Product;
|
||||
|
||||
// 父商品关联
|
||||
@ManyToOne(() => SiteProduct, siteProduct => siteProduct.id)
|
||||
@JoinColumn({ name: 'parentId' })
|
||||
parent: SiteProduct;
|
||||
|
||||
@ApiProperty({
|
||||
example: '2022-12-12 11:11:11',
|
||||
description: '创建时间',
|
||||
required: true,
|
||||
})
|
||||
@CreateDateColumn()
|
||||
createdAt: Date;
|
||||
|
||||
@ApiProperty({
|
||||
example: '2022-12-12 11:11:11',
|
||||
description: '更新时间',
|
||||
required: true,
|
||||
})
|
||||
@UpdateDateColumn()
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
|
@ -42,7 +42,7 @@ export enum OrderStatus {
|
|||
REFUNDED = 'refunded', // 已退款
|
||||
FAILED = 'failed', // 失败订单
|
||||
DRAFT = 'draft', // 草稿
|
||||
AUTO_DRAFT = 'auto-draft', // 自动草稿(TODO:不知道为什么出现)
|
||||
AUTO_DRAFT = 'auto-draft', // 自动草稿(TODO:不知道为什么出现)
|
||||
|
||||
// TRASH = 'trash',
|
||||
// refund 也就是退款相关的状态
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@ export interface IPlatformService {
|
|||
getOrder(siteId: number, orderId: string): Promise<any>;
|
||||
|
||||
/**
|
||||
* 获取订阅列表(如果平台支持)
|
||||
* 获取订阅列表(如果平台支持)
|
||||
* @param siteId 站点ID
|
||||
* @returns 订阅列表数据
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -20,45 +20,70 @@ import { UnifiedPaginationDTO, UnifiedSearchParamsDTO } from '../dto/api.dto';
|
|||
import { BatchOperationDTO, BatchOperationResultDTO } from '../dto/batch.dto';
|
||||
|
||||
export interface ISiteAdapter {
|
||||
// ========== 客户映射方法 ==========
|
||||
/**
|
||||
* 获取产品列表
|
||||
* 将平台客户数据转换为统一客户数据格式
|
||||
* @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;
|
||||
|
||||
/**
|
||||
* 获取媒体列表
|
||||
|
|
@ -75,75 +100,69 @@ export interface ISiteAdapter {
|
|||
*/
|
||||
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>;
|
||||
|
||||
/**
|
||||
* 创建产品变体
|
||||
*/
|
||||
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>;
|
||||
deleteOrder(where: Partial<Pick<UnifiedOrderDTO, 'id'>>): Promise<boolean>;
|
||||
|
||||
/**
|
||||
* 获取订单备注
|
||||
|
|
@ -155,71 +174,6 @@ export interface ISiteAdapter {
|
|||
*/
|
||||
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>;
|
||||
|
||||
/**
|
||||
* 取消订单履行
|
||||
*/
|
||||
|
|
@ -267,4 +221,276 @@ export interface ISiteAdapter {
|
|||
* 删除订单履行信息
|
||||
*/
|
||||
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 }>>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -75,13 +75,20 @@ export class SyncUniuniShipmentJob implements IJob{
|
|||
'255': 'Gateway_To_Gateway_Transit'
|
||||
};
|
||||
async onTick() {
|
||||
const shipments:Shipment[] = await this.shipmentModel.findBy({ finished: false });
|
||||
shipments.forEach(shipment => {
|
||||
this.logisticsService.updateShipmentState(shipment);
|
||||
});
|
||||
const shipments:Shipment[] = await this.shipmentModel.findBy({ finished: false });
|
||||
const results = await Promise.all(
|
||||
shipments.map(async shipment => {
|
||||
return await this.logisticsService.updateShipmentState(shipment);
|
||||
})
|
||||
)
|
||||
this.logger.info(`更新运单状态完毕 ${JSON.stringify(results)}`);
|
||||
return results
|
||||
}
|
||||
|
||||
onComplete(result: any) {
|
||||
|
||||
this.logger.info(`更新运单状态完成 ${result}`);
|
||||
}
|
||||
onError(error: any) {
|
||||
this.logger.error(`更新运单状态失败 ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
import { ILogger, Inject, Logger } from '@midwayjs/core';
|
||||
import { IJob, Job } from '@midwayjs/cron';
|
||||
import { LogisticsService } from '../service/logistics.service';
|
||||
import { Repository } from 'typeorm';
|
||||
import { Shipment } from '../entity/shipment.entity';
|
||||
import { InjectEntityModel } from '@midwayjs/typeorm';
|
||||
|
||||
|
||||
@Job({
|
||||
cronTime: '0 0 12 * * *', // 每天12点执行
|
||||
start: true
|
||||
})
|
||||
export class SyncTmsJob implements IJob {
|
||||
@Logger()
|
||||
logger: ILogger;
|
||||
|
||||
@Inject()
|
||||
logisticsService: LogisticsService;
|
||||
|
||||
@InjectEntityModel(Shipment)
|
||||
shipmentModel: Repository<Shipment>
|
||||
|
||||
async onTick() {
|
||||
const shipments:Shipment[] = await this.shipmentModel.findBy({ tracking_provider: 'freightwaves',finished: false });
|
||||
const results = await Promise.all(
|
||||
shipments.map(async shipment => {
|
||||
return await this.logisticsService.updateFreightwavesShipmentState(shipment);
|
||||
})
|
||||
)
|
||||
this.logger.info(`更新运单状态完毕 ${JSON.stringify(results)}`);
|
||||
return results
|
||||
}
|
||||
|
||||
onComplete(result: any) {
|
||||
this.logger.info(`更新运单状态完成 ${result}`);
|
||||
}
|
||||
onError(error: any) {
|
||||
this.logger.error(`更新运单状态失败 ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
|
@ -21,7 +21,8 @@ export class CategoryService {
|
|||
order: {
|
||||
sort: 'DESC',
|
||||
createdAt: 'DESC'
|
||||
}
|
||||
},
|
||||
relations: ['attributes', 'attributes.attributeDict']
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -66,7 +66,7 @@ export class CustomerService {
|
|||
}
|
||||
|
||||
if (typeof dateValue === 'number') {
|
||||
// 处理Unix时间戳(秒或毫秒)
|
||||
// 处理Unix时间戳(秒或毫秒)
|
||||
return new Date(dateValue > 9999999999 ? dateValue : dateValue * 1000);
|
||||
}
|
||||
|
||||
|
|
@ -95,7 +95,7 @@ export class CustomerService {
|
|||
}
|
||||
|
||||
/**
|
||||
* 创建或更新客户(upsert)
|
||||
* 创建或更新客户(upsert)
|
||||
* 如果客户存在则更新,不存在则创建
|
||||
*/
|
||||
async upsertCustomer(
|
||||
|
|
@ -157,24 +157,24 @@ export class CustomerService {
|
|||
|
||||
/**
|
||||
* 从站点同步客户数据
|
||||
* 第一步:调用adapter获取站点客户数据
|
||||
* 第二步:通过upsertManyCustomers保存这些客户
|
||||
* 第一步:调用adapter获取站点客户数据
|
||||
* 第二步:通过upsertManyCustomers保存这些客户
|
||||
*/
|
||||
async syncCustomersFromSite(
|
||||
siteId: number,
|
||||
params?: UnifiedSearchParamsDTO
|
||||
): Promise<SyncOperationResult> {
|
||||
try {
|
||||
// 第一步:获取适配器并从站点获取客户数据
|
||||
// 第一步:获取适配器并从站点获取客户数据
|
||||
const adapter = await this.siteApiService.getAdapter(siteId);
|
||||
const siteCustomers = await adapter.getAllCustomers(params || {});
|
||||
|
||||
// 第二步:将站点客户数据转换为客户实体数据
|
||||
// 第二步:将站点客户数据转换为客户实体数据
|
||||
const customersData = siteCustomers.map(siteCustomer => {
|
||||
return this.mapSiteCustomerToCustomer(siteCustomer, siteId);
|
||||
})
|
||||
|
||||
// 第三步:批量upsert客户数据
|
||||
// 第三步:批量upsert客户数据
|
||||
const upsertResult = await this.upsertManyCustomers(customersData);
|
||||
return {
|
||||
total: siteCustomers.length,
|
||||
|
|
@ -192,7 +192,7 @@ export class CustomerService {
|
|||
}
|
||||
|
||||
/**
|
||||
* 获取客户统计列表(包含订单统计信息)
|
||||
* 获取客户统计列表(包含订单统计信息)
|
||||
* 支持分页、搜索和排序功能
|
||||
* 使用原生SQL查询实现复杂的统计逻辑
|
||||
*/
|
||||
|
|
@ -363,7 +363,7 @@ export class CustomerService {
|
|||
}
|
||||
|
||||
/**
|
||||
* 获取纯粹的客户列表(不包含订单统计信息)
|
||||
* 获取纯粹的客户列表(不包含订单统计信息)
|
||||
* 支持基本的分页、搜索和排序功能
|
||||
* 使用TypeORM查询构建器实现
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ export class DictService {
|
|||
}
|
||||
|
||||
// 从XLSX文件导入字典
|
||||
async importDictsFromXLSX(bufferOrPath: Buffer | string) {
|
||||
async importDictsFromTable(bufferOrPath: Buffer | string) {
|
||||
// 判断传入的是 Buffer 还是文件路径字符串
|
||||
let buffer: Buffer;
|
||||
if (typeof bufferOrPath === 'string') {
|
||||
|
|
@ -60,7 +60,7 @@ export class DictService {
|
|||
// 如果是 Buffer,直接使用
|
||||
buffer = bufferOrPath;
|
||||
}
|
||||
|
||||
|
||||
// 读取缓冲区中的工作簿
|
||||
const wb = xlsx.read(buffer, { type: 'buffer' });
|
||||
// 获取第一个工作表的名称
|
||||
|
|
@ -93,7 +93,7 @@ export class DictService {
|
|||
|
||||
// 从XLSX文件导入字典项
|
||||
async importDictItemsFromXLSX(bufferOrPath: Buffer | string, dictId: number): Promise<BatchOperationResultDTO> {
|
||||
if(!dictId){
|
||||
if (!dictId) {
|
||||
throw new Error("引入失败, 请输入字典 ID")
|
||||
}
|
||||
|
||||
|
|
@ -101,7 +101,7 @@ export class DictService {
|
|||
if (!dict) {
|
||||
throw new Error('指定的字典不存在');
|
||||
}
|
||||
|
||||
|
||||
// 判断传入的是 Buffer 还是文件路径字符串
|
||||
let buffer: Buffer;
|
||||
if (typeof bufferOrPath === 'string') {
|
||||
|
|
@ -111,7 +111,7 @@ export class DictService {
|
|||
// 如果是 Buffer,直接使用
|
||||
buffer = bufferOrPath;
|
||||
}
|
||||
|
||||
|
||||
const wb = xlsx.read(buffer, { type: 'buffer' });
|
||||
const wsname = wb.SheetNames[0];
|
||||
const ws = wb.Sheets[wsname];
|
||||
|
|
@ -122,7 +122,7 @@ export class DictService {
|
|||
const createdItems = [];
|
||||
const updatedItems = [];
|
||||
const errors = [];
|
||||
|
||||
|
||||
for (const row of data) {
|
||||
try {
|
||||
const result = await this.upsertDictItem(dictId, {
|
||||
|
|
@ -150,7 +150,7 @@ export class DictService {
|
|||
|
||||
const processed = createdItems.length + updatedItems.length;
|
||||
|
||||
return {
|
||||
return {
|
||||
total: data.length,
|
||||
processed: processed,
|
||||
updated: updatedItems.length,
|
||||
|
|
@ -216,10 +216,10 @@ export class DictService {
|
|||
|
||||
// 如果提供了 dictId,则只返回该字典下的项
|
||||
if (params.dictId) {
|
||||
return this.dictItemModel.find({ where });
|
||||
return this.dictItemModel.find({ where, relations: ['dict'] });
|
||||
}
|
||||
// 否则,返回所有字典项
|
||||
return this.dictItemModel.find();
|
||||
return this.dictItemModel.find({ relations: ['dict'] });
|
||||
}
|
||||
|
||||
// 创建新字典项
|
||||
|
|
@ -239,7 +239,7 @@ export class DictService {
|
|||
}
|
||||
|
||||
// 更新或创建字典项 (Upsert)
|
||||
// 如果字典项已存在(根据 name 和 dictId 判断),则更新;否则创建新的
|
||||
// 如果字典项已存在(根据 name 和 dictId 判断),则更新;否则创建新的
|
||||
async upsertDictItem(dictId: number, itemData: {
|
||||
name: string;
|
||||
title: string;
|
||||
|
|
@ -251,8 +251,8 @@ export class DictService {
|
|||
}) {
|
||||
// 格式化 name
|
||||
const formattedName = this.formatName(itemData.name);
|
||||
|
||||
// 查找是否已存在该字典项(根据 name 和 dictId)
|
||||
|
||||
// 查找是否已存在该字典项(根据 name 和 dictId)
|
||||
const existingItem = await this.dictItemModel.findOne({
|
||||
where: {
|
||||
name: formattedName,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,332 @@
|
|||
import { Inject, Provide } from '@midwayjs/core';
|
||||
import axios from 'axios';
|
||||
import * as crypto from 'crypto';
|
||||
import dayjs = require('dayjs');
|
||||
import utc = require('dayjs/plugin/utc');
|
||||
import timezone = require('dayjs/plugin/timezone');
|
||||
|
||||
// 扩展dayjs功能
|
||||
dayjs.extend(utc);
|
||||
dayjs.extend(timezone);
|
||||
|
||||
// 全局参数配置接口
|
||||
interface FreightwavesConfig {
|
||||
appSecret: string;
|
||||
apiBaseUrl: string;
|
||||
partner: string;
|
||||
}
|
||||
|
||||
// 地址信息接口
|
||||
interface Address {
|
||||
name: string;
|
||||
phone: string;
|
||||
company: string;
|
||||
countryCode: string;
|
||||
city: string;
|
||||
state: string;
|
||||
address1: string;
|
||||
address2: string;
|
||||
postCode: string;
|
||||
zoneCode?: string;
|
||||
countryName: string;
|
||||
cityName: string;
|
||||
stateName: string;
|
||||
companyName: string;
|
||||
}
|
||||
|
||||
// 包裹尺寸接口
|
||||
interface Dimensions {
|
||||
length: number;
|
||||
width: number;
|
||||
height: number;
|
||||
lengthUnit: 'IN' | 'CM';
|
||||
weight: number;
|
||||
weightUnit: 'LB' | 'KG';
|
||||
}
|
||||
|
||||
// 包裹信息接口
|
||||
interface Package {
|
||||
dimensions: Dimensions;
|
||||
currency: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
// 申报信息接口
|
||||
interface Declaration {
|
||||
boxNo: string;
|
||||
sku: string;
|
||||
cnname: string;
|
||||
enname: string;
|
||||
declaredPrice: number;
|
||||
declaredQty: number;
|
||||
material: string;
|
||||
intendedUse: string;
|
||||
cweight: number;
|
||||
hsCode: string;
|
||||
battery: string;
|
||||
}
|
||||
|
||||
// 费用试算请求接口
|
||||
export interface RateTryRequest {
|
||||
shipCompany: string;
|
||||
partnerOrderNumber: string;
|
||||
warehouseId?: string;
|
||||
shipper: Address;
|
||||
reciver: Address;
|
||||
packages: Package[];
|
||||
partner: string;
|
||||
signService?: 0 | 1;
|
||||
}
|
||||
|
||||
// 创建订单请求接口
|
||||
interface CreateOrderRequest extends RateTryRequest {
|
||||
declaration: Declaration;
|
||||
}
|
||||
|
||||
// 查询订单请求接口
|
||||
interface QueryOrderRequest {
|
||||
partnerOrderNumber?: string;
|
||||
shipOrderId?: string;
|
||||
partner: string;
|
||||
}
|
||||
|
||||
// 修改订单请求接口
|
||||
interface ModifyOrderRequest extends CreateOrderRequest {
|
||||
shipOrderId: string;
|
||||
}
|
||||
|
||||
// 订单退款请求接口
|
||||
interface RefundOrderRequest {
|
||||
shipOrderId: string;
|
||||
partner: string;
|
||||
}
|
||||
|
||||
// 通用响应接口
|
||||
interface FreightwavesResponse<T> {
|
||||
code: string;
|
||||
msg: string;
|
||||
data: T;
|
||||
}
|
||||
|
||||
// 费用试算响应数据接口
|
||||
interface RateTryResponseData {
|
||||
shipCompany: string;
|
||||
channelCode: string;
|
||||
totalAmount: number;
|
||||
currency: string;
|
||||
}
|
||||
|
||||
// 创建订单响应数据接口
|
||||
interface CreateOrderResponseData {
|
||||
msg: string;
|
||||
data: any;
|
||||
}
|
||||
|
||||
// 查询订单响应数据接口
|
||||
interface QueryOrderResponseData {
|
||||
thirdOrderId: string;
|
||||
shipCompany: string;
|
||||
expressFinish: 0 | 1 | 2;
|
||||
expressFailMsg: string;
|
||||
expressOrder: {
|
||||
mainTrackingNumber: string;
|
||||
labelPath: string[];
|
||||
totalAmount: number;
|
||||
currency: string;
|
||||
balance: number;
|
||||
};
|
||||
partnerOrderNumber: string;
|
||||
shipOrderId: string;
|
||||
}
|
||||
|
||||
// 修改订单响应数据接口
|
||||
interface ModifyOrderResponseData extends CreateOrderResponseData { }
|
||||
|
||||
// 订单退款响应数据接口
|
||||
interface RefundOrderResponseData { }
|
||||
|
||||
@Provide()
|
||||
export class FreightwavesService {
|
||||
@Inject() logger;
|
||||
|
||||
// 默认配置
|
||||
private config: FreightwavesConfig = {
|
||||
appSecret: 'gELCHguGmdTLo!zfihfM91hae8G@9Sz23Mh6pHrt',
|
||||
apiBaseUrl: 'http://tms.freightwaves.ca:8901',
|
||||
partner: '25072621035200000060'
|
||||
};
|
||||
|
||||
// 初始化配置
|
||||
setConfig(config: Partial<FreightwavesConfig>): void {
|
||||
this.config = { ...this.config, ...config };
|
||||
}
|
||||
|
||||
// 生成签名
|
||||
private generateSignature(body: any, date: string): string {
|
||||
const bodyString = JSON.stringify(body);
|
||||
const signatureStr = `${bodyString}${this.config.appSecret}${date}`;
|
||||
return crypto.createHash('md5').update(signatureStr).digest('hex');
|
||||
}
|
||||
|
||||
// 发送请求
|
||||
private async sendRequest<T>(url: string, data: any): Promise<FreightwavesResponse<T>> {
|
||||
try {
|
||||
// 设置请求头 - 使用太平洋时间 (America/Los_Angeles)
|
||||
const date = dayjs().tz('America/Los_Angeles').format('YYYY-MM-DD HH:mm:ss');
|
||||
const headers = {
|
||||
'Content-Type': 'application/json',
|
||||
'requestDate': date,
|
||||
'signature': this.generateSignature(data, date),
|
||||
};
|
||||
|
||||
// 发送请求 - 临时禁用SSL证书验证以解决UNABLE_TO_VERIFY_LEAF_SIGNATURE错误
|
||||
const response = await axios.post<FreightwavesResponse<T>>(
|
||||
`${this.config.apiBaseUrl}${url}`,
|
||||
data,
|
||||
{
|
||||
headers,
|
||||
httpsAgent: new (require('https').Agent)({
|
||||
rejectUnauthorized: false
|
||||
})
|
||||
}
|
||||
);
|
||||
|
||||
// 记录响应信息
|
||||
this.log(`Received response from: ${this.config.apiBaseUrl}${url}`, {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
data: response.data
|
||||
});
|
||||
|
||||
// 处理响应
|
||||
if (response.data.code !== '00000200') {
|
||||
this.log(`Freightwaves API error: ${response.data.msg}`, { url, data, response: response.data });
|
||||
throw new Error(response.data.msg);
|
||||
}
|
||||
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
// 更详细的错误记录
|
||||
if (error.response) {
|
||||
// 请求已发送,服务器返回错误状态码
|
||||
this.log(`Freightwaves API request failed with status: ${error.response.status}`, {
|
||||
url,
|
||||
data,
|
||||
response: error.response.data,
|
||||
status: error.response.status,
|
||||
headers: error.response.headers
|
||||
});
|
||||
} else if (error.request) {
|
||||
// 请求已发送,但没有收到响应
|
||||
this.log(`Freightwaves API request no response received`, {
|
||||
url,
|
||||
data,
|
||||
request: error.request
|
||||
});
|
||||
} else {
|
||||
// 请求配置时发生错误
|
||||
this.log(`Freightwaves API request configuration error: ${error.message}`, {
|
||||
url,
|
||||
data,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 费用试算
|
||||
* @param params 费用试算参数
|
||||
* @returns 费用试算结果
|
||||
*/
|
||||
async rateTry(params: Omit<RateTryRequest, 'partner'>): Promise<RateTryResponseData> {
|
||||
const requestData: RateTryRequest = {
|
||||
...params,
|
||||
partner: this.config.partner,
|
||||
};
|
||||
|
||||
const response = await this.sendRequest<RateTryResponseData>('/shipService/order/rateTry', requestData);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建订单
|
||||
* @param params 创建订单参数
|
||||
* @returns 创建订单结果
|
||||
*/
|
||||
async createOrder(params: Omit<CreateOrderRequest, 'partner'>): Promise<CreateOrderResponseData> {
|
||||
const requestData: CreateOrderRequest = {
|
||||
...params,
|
||||
partner: this.config.partner,
|
||||
};
|
||||
|
||||
const response = await this.sendRequest<CreateOrderResponseData>('/shipService/order/createOrder', requestData);
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询订单
|
||||
* @param params 查询订单参数
|
||||
* @returns 查询订单结果
|
||||
*/
|
||||
async queryOrder(params: Omit<QueryOrderRequest, 'partner'>): Promise<QueryOrderResponseData> {
|
||||
const requestData: QueryOrderRequest = {
|
||||
...params,
|
||||
partner: this.config.partner,
|
||||
};
|
||||
|
||||
const response = await this.sendRequest<QueryOrderResponseData>('/shipService/order/queryOrder', requestData);
|
||||
if (response.code !== '00000200') {
|
||||
throw new Error(response.msg);
|
||||
}
|
||||
return response.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改订单
|
||||
* @param params 修改订单参数
|
||||
* @returns 修改订单结果
|
||||
*/
|
||||
async modifyOrder(params: Omit<ModifyOrderRequest, 'partner'>): Promise<ModifyOrderResponseData> {
|
||||
const requestData: ModifyOrderRequest = {
|
||||
...params,
|
||||
partner: this.config.partner,
|
||||
};
|
||||
|
||||
const response = await this.sendRequest<ModifyOrderResponseData>('/shipService/order/modifyOrder', requestData);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* 订单退款
|
||||
* @param params 订单退款参数
|
||||
* @returns 订单退款结果
|
||||
*/
|
||||
async refundOrder(params: Omit<RefundOrderRequest, 'partner'>): Promise<RefundOrderResponseData> {
|
||||
const requestData: RefundOrderRequest = {
|
||||
...params,
|
||||
partner: this.config.partner,
|
||||
};
|
||||
const response = await this.sendRequest<RefundOrderResponseData>('/shipService/order/refundOrder', requestData);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* 辅助日志方法,处理logger可能未定义的情况
|
||||
* @param message 日志消息
|
||||
* @param data 附加数据
|
||||
*/
|
||||
private log(message: string, data?: any) {
|
||||
if (this.logger) {
|
||||
this.logger.info(message, data);
|
||||
} else {
|
||||
// 如果logger未定义,使用console输出
|
||||
if (data) {
|
||||
console.log(message, data);
|
||||
} else {
|
||||
console.log(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -27,10 +27,12 @@ import { CanadaPostService } from './canadaPost.service';
|
|||
import { OrderItem } from '../entity/order_item.entity';
|
||||
import { OrderSale } from '../entity/order_sale.entity';
|
||||
import { UniExpressService } from './uni_express.service';
|
||||
import { FreightwavesService, RateTryRequest } from './freightwaves.service';
|
||||
import { StockPoint } from '../entity/stock_point.entity';
|
||||
import { OrderService } from './order.service';
|
||||
import { convertKeysFromCamelToSnake } from '../utils/object-transform.util';
|
||||
import { SiteService } from './site.service';
|
||||
import { ShopyyService } from './shopyy.service';
|
||||
|
||||
@Provide()
|
||||
export class LogisticsService {
|
||||
|
|
@ -73,9 +75,15 @@ export class LogisticsService {
|
|||
@Inject()
|
||||
uniExpressService: UniExpressService;
|
||||
|
||||
@Inject()
|
||||
freightwavesService: FreightwavesService;
|
||||
|
||||
@Inject()
|
||||
wpService: WPService;
|
||||
|
||||
@Inject()
|
||||
shopyyService: ShopyyService;
|
||||
|
||||
@Inject()
|
||||
orderService: OrderService;
|
||||
|
||||
|
|
@ -125,6 +133,10 @@ export class LogisticsService {
|
|||
try {
|
||||
const data = await this.uniExpressService.getOrderStatus(shipment.return_tracking_number);
|
||||
console.log('updateShipmentState data:', data);
|
||||
// huo
|
||||
if (data.status === 'FAIL') {
|
||||
throw new Error('获取运单状态失败,原因为' + data.ret_msg)
|
||||
}
|
||||
shipment.state = data.data[0].state;
|
||||
if (shipment.state in [203, 215, 216, 230]) { // todo,写常数
|
||||
shipment.finished = true;
|
||||
|
|
@ -137,6 +149,30 @@ export class LogisticsService {
|
|||
}
|
||||
}
|
||||
|
||||
//"expressFinish": 0, //是否快递创建完成(1:完成 0:未完成,需要轮询 2:失败)
|
||||
async updateFreightwavesShipmentState(shipment: Shipment) {
|
||||
try {
|
||||
const data = await this.freightwavesService.queryOrder({ shipOrderId: shipment.order_id.toString() });
|
||||
console.log('updateFreightwavesShipmentState data:', data);
|
||||
// huo
|
||||
if (data.expressFinish === 2) {
|
||||
throw new Error('获取运单状态失败,原因为' + data.expressFailMsg)
|
||||
}
|
||||
|
||||
if (data.expressFinish === 0) {
|
||||
shipment.state = '203';
|
||||
shipment.finished = true;
|
||||
}
|
||||
|
||||
this.shipmentModel.save(shipment);
|
||||
return shipment.state;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
// throw new Error(`更新运单状态失败 ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async updateShipmentStateById(id: number) {
|
||||
const shipment: Shipment = await this.shipmentModel.findOneBy({ id: id });
|
||||
return this.updateShipmentState(shipment);
|
||||
|
|
@ -243,8 +279,7 @@ export class LogisticsService {
|
|||
|
||||
shipmentRepo.remove(shipment);
|
||||
|
||||
const res = await this.uniExpressService.deleteShipment(shipment.return_tracking_number);
|
||||
console.log('res', res.data); // todo
|
||||
await this.uniExpressService.deleteShipment(shipment.return_tracking_number);
|
||||
|
||||
await orderRepo.save(order);
|
||||
|
||||
|
|
@ -274,7 +309,6 @@ export class LogisticsService {
|
|||
console.log('同步到woocommerce失败', error);
|
||||
return true;
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch {
|
||||
throw new Error('删除运单失败');
|
||||
|
|
@ -290,7 +324,16 @@ export class LogisticsService {
|
|||
currency: 'CAD',
|
||||
// item_description: data.sales, // todo: 货品信息
|
||||
}
|
||||
const resShipmentFee = await this.uniExpressService.getRates(reqBody);
|
||||
let resShipmentFee: any;
|
||||
if (data.shipmentPlatform === 'uniuni') {
|
||||
resShipmentFee = await this.uniExpressService.getRates(reqBody);
|
||||
} else if (data.shipmentPlatform === 'freightwaves') {
|
||||
const fre_reqBody = await this.convertToFreightwavesRateTry(data);
|
||||
resShipmentFee = await this.freightwavesService.rateTry(fre_reqBody);
|
||||
} else {
|
||||
throw new Error('不支持的运单平台');
|
||||
}
|
||||
|
||||
if (resShipmentFee.status !== 'SUCCESS') {
|
||||
throw new Error(resShipmentFee.ret_msg);
|
||||
}
|
||||
|
|
@ -315,40 +358,10 @@ export class LogisticsService {
|
|||
|
||||
let resShipmentOrder;
|
||||
try {
|
||||
const stock_point = await this.stockPointModel.findOneBy({ id: data.stockPointId });
|
||||
const reqBody = {
|
||||
sender: data.details.origin.contact_name,
|
||||
start_phone: data.details.origin.phone_number,
|
||||
start_postal_code: data.details.origin.address.postal_code.replace(/\s/g, ''),
|
||||
pickup_address: data.details.origin.address.address_line_1,
|
||||
pickup_warehouse: stock_point.upStreamStockPointId,
|
||||
shipper_country_code: data.details.origin.address.country,
|
||||
receiver: data.details.destination.contact_name,
|
||||
city: data.details.destination.address.city,
|
||||
province: data.details.destination.address.region,
|
||||
country: data.details.destination.address.country,
|
||||
postal_code: data.details.destination.address.postal_code.replace(/\s/g, ''),
|
||||
delivery_address: data.details.destination.address.address_line_1,
|
||||
receiver_phone: data.details.destination.phone_number.number,
|
||||
receiver_email: data.details.destination.email_addresses,
|
||||
// item_description: data.sales, // todo: 货品信息
|
||||
length: data.details.packaging_properties.packages[0].measurements.cuboid.l,
|
||||
width: data.details.packaging_properties.packages[0].measurements.cuboid.w,
|
||||
height: data.details.packaging_properties.packages[0].measurements.cuboid.h,
|
||||
dimension_uom: data.details.packaging_properties.packages[0].measurements.cuboid.unit,
|
||||
weight: data.details.packaging_properties.packages[0].measurements.weight.value,
|
||||
weight_uom: data.details.packaging_properties.packages[0].measurements.weight.unit,
|
||||
currency: 'CAD',
|
||||
custom_field: {
|
||||
'order_id': order.externalOrderId
|
||||
}
|
||||
}
|
||||
resShipmentOrder = await this.mepShipment(data, order);
|
||||
|
||||
// 添加运单
|
||||
resShipmentOrder = await this.uniExpressService.createShipment(reqBody);
|
||||
|
||||
// 记录物流信息,并将订单状态转到完成
|
||||
if (resShipmentOrder.status === 'SUCCESS') {
|
||||
// 记录物流信息,并将订单状态转到完成,uniuni状态为SUCCESS,tms.freightwaves状态为00000200
|
||||
if (resShipmentOrder.status === 'SUCCESS' || resShipmentOrder.code === '00000200') {
|
||||
order.orderStatus = ErpOrderStatus.COMPLETED;
|
||||
} else {
|
||||
throw new Error('运单生成失败');
|
||||
|
|
@ -359,49 +372,89 @@ export class LogisticsService {
|
|||
await dataSource.transaction(async manager => {
|
||||
const orderRepo = manager.getRepository(Order);
|
||||
const shipmentRepo = manager.getRepository(Shipment);
|
||||
const tracking_provider = 'UniUni'; // todo: id未确定,后写进常数
|
||||
const tracking_provider = data.shipmentPlatform; // todo: id未确定,后写进常数
|
||||
|
||||
// 同步物流信息到woocommerce
|
||||
const site = await this.siteService.get(Number(order.siteId), true);
|
||||
const res = await this.wpService.createFulfillment(site, order.externalOrderId, {
|
||||
tracking_number: resShipmentOrder.data.tno,
|
||||
tracking_provider: tracking_provider,
|
||||
});
|
||||
|
||||
if (order.orderStatus === ErpOrderStatus.COMPLETED) {
|
||||
const shipment = await shipmentRepo.save({
|
||||
tracking_provider: tracking_provider,
|
||||
tracking_id: res.data.tracking_id,
|
||||
unique_id: resShipmentOrder.data.uni_order_sn,
|
||||
stockPointId: String(data.stockPointId), // todo
|
||||
state: resShipmentOrder.data.uni_status_code,
|
||||
return_tracking_number: resShipmentOrder.data.tno,
|
||||
fee: data.details.shipmentFee,
|
||||
order: order
|
||||
});
|
||||
order.shipmentId = shipment.id;
|
||||
shipmentId = shipment.id;
|
||||
let co: any;
|
||||
let unique_id: any;
|
||||
let state: any;
|
||||
if (data.shipmentPlatform === 'uniuni') {
|
||||
co = resShipmentOrder.data.tno;
|
||||
unique_id = resShipmentOrder.data.uni_order_sn;
|
||||
state = resShipmentOrder.data.uni_status_code;
|
||||
} else {
|
||||
co = resShipmentOrder.data?.shipOrderId;
|
||||
unique_id = resShipmentOrder.data?.shipOrderId;
|
||||
state = ErpOrderStatus.COMPLETED;
|
||||
}
|
||||
|
||||
// 同步订单状态到woocommerce
|
||||
if (order.status !== OrderStatus.COMPLETED) {
|
||||
await this.wpService.updateOrder(site, order.externalOrderId, {
|
||||
status: OrderStatus.COMPLETED,
|
||||
if (order.source_type != "shopyy") {
|
||||
const res = await this.wpService.createFulfillment(site, order.externalOrderId, {
|
||||
tracking_number: co,
|
||||
tracking_provider: tracking_provider,
|
||||
});
|
||||
order.status = OrderStatus.COMPLETED;
|
||||
|
||||
if (order.orderStatus === ErpOrderStatus.COMPLETED) {
|
||||
const shipment = await shipmentRepo.save({
|
||||
tracking_provider: tracking_provider,
|
||||
tracking_id: res.data.tracking_id,
|
||||
unique_id: unique_id,
|
||||
stockPointId: String(data.stockPointId), // todo
|
||||
state: state,
|
||||
return_tracking_number: co,
|
||||
fee: data.details.shipmentFee,
|
||||
order: order
|
||||
});
|
||||
order.shipmentId = shipment.id;
|
||||
shipmentId = shipment.id;
|
||||
}
|
||||
if (order.status !== OrderStatus.COMPLETED) {
|
||||
await this.wpService.updateOrder(site, order.externalOrderId, {
|
||||
status: OrderStatus.COMPLETED,
|
||||
});
|
||||
order.status = OrderStatus.COMPLETED;
|
||||
}
|
||||
}
|
||||
if (order.source_type === "shopyy") {
|
||||
const res = await this.shopyyService.createFulfillment(site, order.externalOrderId, {
|
||||
tracking_number: co,
|
||||
tracking_company: resShipmentOrder.shipCompany,
|
||||
carrier_code: resShipmentOrder.shipperOrderId,
|
||||
});
|
||||
if (order.orderStatus === ErpOrderStatus.COMPLETED) {
|
||||
const shipment = await shipmentRepo.save({
|
||||
tracking_provider: tracking_provider,
|
||||
tracking_id: res.data.tracking_id,
|
||||
unique_id: unique_id,
|
||||
stockPointId: String(data.stockPointId), // todo
|
||||
state: state,
|
||||
return_tracking_number: co,
|
||||
fee: data.details.shipmentFee,
|
||||
order: order
|
||||
});
|
||||
order.shipmentId = shipment.id;
|
||||
shipmentId = shipment.id;
|
||||
}
|
||||
if (order.status !== OrderStatus.COMPLETED) {
|
||||
// shopyy未提供更新订单接口,暂不更新订单状态
|
||||
// await this.shopyyService.updateOrder(site, order.externalOrderId, {
|
||||
// status: OrderStatus.COMPLETED,
|
||||
// });
|
||||
order.status = OrderStatus.COMPLETED;
|
||||
}
|
||||
}
|
||||
order.orderStatus = ErpOrderStatus.COMPLETED;
|
||||
|
||||
await orderRepo.save(order);
|
||||
}).catch(error => {
|
||||
transactionError = error
|
||||
throw new Error(`请求错误:${error}`);
|
||||
});
|
||||
|
||||
if (transactionError !== undefined) {
|
||||
console.log('err', transactionError);
|
||||
throw transactionError;
|
||||
}
|
||||
|
||||
// 更新产品发货信息
|
||||
this.orderService.updateOrderSales(order.id, sales);
|
||||
|
||||
|
|
@ -638,4 +691,190 @@ export class LogisticsService {
|
|||
|
||||
return { items, total, current, pageSize };
|
||||
}
|
||||
|
||||
|
||||
async mepShipment(data: ShipmentBookDTO, order: Order) {
|
||||
try {
|
||||
const stock_point = await this.stockPointModel.findOneBy({ id: data.stockPointId });
|
||||
let resShipmentOrder;
|
||||
|
||||
if (data.shipmentPlatform === 'uniuni') {
|
||||
const reqBody = {
|
||||
sender: data.details.origin.contact_name,
|
||||
start_phone: data.details.origin.phone_number,
|
||||
start_postal_code: data.details.origin.address.postal_code.replace(/\s/g, ''),
|
||||
pickup_address: data.details.origin.address.address_line_1,
|
||||
pickup_warehouse: stock_point.upStreamStockPointId,
|
||||
shipper_country_code: data.details.origin.address.country,
|
||||
receiver: data.details.destination.contact_name,
|
||||
city: data.details.destination.address.city,
|
||||
province: data.details.destination.address.region,
|
||||
country: data.details.destination.address.country,
|
||||
postal_code: data.details.destination.address.postal_code.replace(/\s/g, ''),
|
||||
delivery_address: data.details.destination.address.address_line_1,
|
||||
receiver_phone: data.details.destination.phone_number.number,
|
||||
receiver_email: data.details.destination.email_addresses,
|
||||
// item_description: data.sales, // todo: 货品信息
|
||||
length: data.details.packaging_properties.packages[0].measurements.cuboid.l,
|
||||
width: data.details.packaging_properties.packages[0].measurements.cuboid.w,
|
||||
height: data.details.packaging_properties.packages[0].measurements.cuboid.h,
|
||||
dimension_uom: data.details.packaging_properties.packages[0].measurements.cuboid.unit,
|
||||
weight: data.details.packaging_properties.packages[0].measurements.weight.value,
|
||||
weight_uom: data.details.packaging_properties.packages[0].measurements.weight.unit,
|
||||
currency: 'CAD',
|
||||
custom_field: {
|
||||
'order_id': order.externalOrderId // todo: 需要获取订单的externalOrderId
|
||||
}
|
||||
};
|
||||
// 添加运单
|
||||
resShipmentOrder = await this.uniExpressService.createShipment(reqBody);
|
||||
}
|
||||
|
||||
if (data.shipmentPlatform === 'freightwaves') {
|
||||
// 根据TMS系统对接说明文档格式化参数
|
||||
const reqBody: any = {
|
||||
shipCompany: 'UPSYYZ7000NEW',
|
||||
partnerOrderNumber: order.siteId + '-' + order.externalOrderId,
|
||||
warehouseId: '25072621030107400060',
|
||||
shipper: {
|
||||
name: data.details.origin.contact_name, // 姓名
|
||||
phone: data.details.origin.phone_number.number, // 电话(提取number属性转换为字符串)
|
||||
company: '', // 公司
|
||||
countryCode: data.details.origin.address.country, // 国家Code
|
||||
city: data.details.origin.address.city, // 城市
|
||||
state: data.details.origin.address.region, // 州/省Code,两个字母缩写
|
||||
address1: data.details.origin.address.address_line_1, // 详细地址
|
||||
address2: '', // 详细地址2(Address类型中没有address_line_2属性)
|
||||
postCode: data.details.origin.address.postal_code.replace(/\s/g, ''), // 邮编
|
||||
countryName: data.details.origin.address.country, // 国家名称(Address类型中没有country_name属性,使用country代替)
|
||||
cityName: data.details.origin.address.city, // 城市名称
|
||||
stateName: data.details.origin.address.region, // 州/省名称
|
||||
companyName: '' // 公司名称
|
||||
},
|
||||
reciver: {
|
||||
name: data.details.destination.contact_name, // 姓名
|
||||
phone: data.details.destination.phone_number.number, // 电话
|
||||
company: '', // 公司
|
||||
countryCode: data.details.destination.address.country, // 国家Code
|
||||
city: data.details.destination.address.city, // 城市
|
||||
state: data.details.destination.address.region, // 州/省Code,两个字母的缩写
|
||||
address1: data.details.destination.address.address_line_1, // 详细地址
|
||||
address2: '', // 详细地址2(Address类型中没有address_line_2属性)
|
||||
postCode: data.details.destination.address.postal_code.replace(/\s/g, ''), // 邮编
|
||||
countryName: data.details.destination.address.country, // 国家名称(Address类型中没有country_name属性,使用country代替)
|
||||
cityName: data.details.destination.address.city, // 城市名称
|
||||
stateName: data.details.destination.address.region, // 州/省名称
|
||||
companyName: '' // 公司名称
|
||||
},
|
||||
packages: [
|
||||
{
|
||||
dimensions: {
|
||||
length: data.details.packaging_properties.packages[0].measurements.cuboid.l, // 长
|
||||
width: data.details.packaging_properties.packages[0].measurements.cuboid.w, // 宽
|
||||
height: data.details.packaging_properties.packages[0].measurements.cuboid.h, // 高
|
||||
lengthUnit: (data.details.packaging_properties.packages[0].measurements.cuboid.unit === 'cm' ? 'CM' : 'IN') as 'CM' | 'IN', // 长度单位(IN,CM)
|
||||
weight: data.details.packaging_properties.packages[0].measurements.weight.value, // 重量
|
||||
weightUnit: (data.details.packaging_properties.packages[0].measurements.weight.unit === 'kg' ? 'KG' : 'LB') as 'KG' | 'LB' // 重量单位(LB,KG)
|
||||
},
|
||||
currency: 'CAD', // 币种(默认CAD)
|
||||
description: 'site:' + order.siteId + ' orderId:' + order.externalOrderId // 包裹描述(确保是字符串类型)
|
||||
}
|
||||
],
|
||||
signService: 0
|
||||
// 非跨境订单不需要declaration
|
||||
// declaration: {
|
||||
// "boxNo": "", //箱子编号
|
||||
// "sku": "", //SKU
|
||||
// "cnname": "", //中文名称
|
||||
// "enname": "", //英文名称
|
||||
// "declaredPrice": 1, //申报单价
|
||||
// "declaredQty": 1, //申报数量
|
||||
// "material": "", //材质
|
||||
// "intendedUse": "", //用途
|
||||
// "cweight": 1, //产品单重
|
||||
// "hsCode": "", //海关编码
|
||||
// "battery": "" //电池描述
|
||||
// }
|
||||
};
|
||||
|
||||
resShipmentOrder = await this.freightwavesService.createOrder(reqBody); // 创建订单
|
||||
//tms只返回了物流订单号,需要查询一次来获取完整的物流信息
|
||||
const queryRes = await this.freightwavesService.queryOrder({ shipOrderId: resShipmentOrder.shipOrderId }); // 查询订单
|
||||
resShipmentOrder.push(queryRes);
|
||||
}
|
||||
|
||||
return resShipmentOrder;
|
||||
} catch (error) {
|
||||
console.log('物流订单处理失败:', error); // 使用console.log代替this.log
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将ShipmentFeeBookDTO转换为freightwaves的RateTryRequest格式
|
||||
* @param data ShipmentFeeBookDTO数据
|
||||
* @returns RateTryRequest格式的数据
|
||||
*/
|
||||
async convertToFreightwavesRateTry(data: ShipmentFeeBookDTO): Promise<Omit<RateTryRequest, 'partner'>> {
|
||||
|
||||
const shipments = await this.shippingAddressModel.findOne({
|
||||
where: {
|
||||
id: data.address_id,
|
||||
},
|
||||
})
|
||||
|
||||
const address = shipments?.address;
|
||||
// 转换为RateTryRequest格式
|
||||
const r = {
|
||||
shipCompany: 'UPSYYZ7000NEW', // 必填,但ShipmentFeeBookDTO中缺少
|
||||
partnerOrderNumber: `order-${Date.now()}`, // 必填,使用时间戳生成
|
||||
warehouseId: '25072621030107400060', // 可选,使用stockPointId转换
|
||||
shipper: {
|
||||
name: data.sender, // 必填
|
||||
phone: data.startPhone.phone, // 必填
|
||||
company: address.country, // 必填,但ShipmentFeeBookDTO中缺少
|
||||
countryCode: data.shipperCountryCode, // 必填
|
||||
city: address.city || '', // 必填,但ShipmentFeeBookDTO中缺少
|
||||
state: address.region || '', // 必填,但ShipmentFeeBookDTO中缺少
|
||||
address1: address.address_line_1, // 必填
|
||||
address2: address.address_line_1 || '', // 必填,但ShipmentFeeBookDTO中缺少
|
||||
postCode: data.startPostalCode, // 必填
|
||||
countryName: address.country || '', // 必填,但ShipmentFeeBookDTO中缺少
|
||||
cityName: address.city || '', // 必填,但ShipmentFeeBookDTO中缺少
|
||||
stateName: address.region || '', // 必填,但ShipmentFeeBookDTO中缺少
|
||||
companyName: address.country || '', // 必填,但ShipmentFeeBookDTO中缺少
|
||||
},
|
||||
reciver: {
|
||||
name: data.receiver, // 必填
|
||||
phone: data.receiverPhone, // 必填
|
||||
company: address.country,// 必填,但ShipmentFeeBookDTO中缺少
|
||||
countryCode: data.country, // 必填,使用country代替countryCode
|
||||
city: data.city, // 必填
|
||||
state: data.province, // 必填,使用province代替state
|
||||
address1: data.deliveryAddress, // 必填
|
||||
address2: data.deliveryAddress, // 必填,但ShipmentFeeBookDTO中缺少
|
||||
postCode: data.postalCode, // 必填
|
||||
countryName: address.country, // 必填,但ShipmentFeeBookDTO中缺少
|
||||
cityName: data.city || '', // 必填,使用city代替cityName
|
||||
stateName: data.province || '', // 必填,使用province代替stateName
|
||||
companyName: address.country || '', // 必填,但ShipmentFeeBookDTO中缺少
|
||||
},
|
||||
packages: [
|
||||
{
|
||||
dimensions: {
|
||||
length: data.length, // 必填
|
||||
width: data.width, // 必填
|
||||
height: data.height, // 必填
|
||||
lengthUnit: (data.dimensionUom === 'IN' ? 'IN' : 'CM') as 'IN' | 'CM', // 必填,转换为有效的单位
|
||||
weight: data.weight, // 必填
|
||||
weightUnit: (data.weightUom === 'LBS' ? 'LB' : 'KG') as 'LB' | 'KG', // 必填,转换为有效的单位
|
||||
},
|
||||
currency: 'CAD', // 必填,但ShipmentFeeBookDTO中缺少,使用默认值
|
||||
description: 'Package', // 必填,但ShipmentFeeBookDTO中缺少,使用默认值
|
||||
},
|
||||
],
|
||||
signService: 0, // 可选,默认不使用签名服务
|
||||
};
|
||||
return r as any;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ import * as path from 'path';
|
|||
import * as os from 'os';
|
||||
import { UnifiedOrderDTO } from '../dto/site-api.dto';
|
||||
import { CustomerService } from './customer.service';
|
||||
import { ProductService } from './product.service';
|
||||
@Provide()
|
||||
export class OrderService {
|
||||
|
||||
|
|
@ -110,7 +111,9 @@ export class OrderService {
|
|||
|
||||
@Logger()
|
||||
logger; // 注入 Logger 实例
|
||||
|
||||
@Inject()
|
||||
productService: ProductService;
|
||||
|
||||
/**
|
||||
* 批量同步订单
|
||||
* 流程说明:
|
||||
|
|
@ -138,7 +141,7 @@ export class OrderService {
|
|||
updated: 0,
|
||||
errors: []
|
||||
};
|
||||
|
||||
this.logger.info('开始进入循环同步订单', result.length, '个订单')
|
||||
// 遍历每个订单进行同步
|
||||
for (const order of result) {
|
||||
try {
|
||||
|
|
@ -146,8 +149,8 @@ export class OrderService {
|
|||
const existingOrder = await this.orderModel.findOne({
|
||||
where: { externalOrderId: String(order.id), siteId: siteId },
|
||||
});
|
||||
if(!existingOrder){
|
||||
console.log("数据库中不存在",order.id, '订单状态:', order.status )
|
||||
if (!existingOrder) {
|
||||
this.logger.debug("数据库中不存在", order.id, '订单状态:', order.status)
|
||||
}
|
||||
// 同步单个订单
|
||||
await this.syncSingleOrder(siteId, order);
|
||||
|
|
@ -162,6 +165,7 @@ export class OrderService {
|
|||
} else {
|
||||
syncResult.created++;
|
||||
}
|
||||
// console.log('updated', syncResult.updated, 'created:', syncResult.created)
|
||||
} catch (error) {
|
||||
// 记录错误但不中断整个同步过程
|
||||
syncResult.errors.push({
|
||||
|
|
@ -171,7 +175,7 @@ export class OrderService {
|
|||
syncResult.processed++;
|
||||
}
|
||||
}
|
||||
this.logger.debug('syncOrders result', syncResult)
|
||||
this.logger.info('同步完成', syncResult.updated, 'created:', syncResult.created)
|
||||
return syncResult;
|
||||
}
|
||||
|
||||
|
|
@ -202,14 +206,14 @@ export class OrderService {
|
|||
try {
|
||||
// 调用 WooCommerce API 获取订单
|
||||
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({
|
||||
where: { externalOrderId: String(order.id), siteId: siteId },
|
||||
});
|
||||
if(!existingOrder){
|
||||
console.log("数据库不存在", siteId , "订单:",order.id, '订单状态:' + order.status )
|
||||
if (!existingOrder) {
|
||||
this.logger.debug("数据库不存在", siteId, "订单:", order.id, '订单状态:' + order.status)
|
||||
}
|
||||
// 同步单个订单
|
||||
await this.syncSingleOrder(siteId, order, true);
|
||||
|
|
@ -268,7 +272,7 @@ export class OrderService {
|
|||
try {
|
||||
const site = await this.siteService.get(siteId);
|
||||
// 仅处理 WooCommerce 站点
|
||||
if(site.type !== 'woocommerce'){
|
||||
if (site.type !== 'woocommerce') {
|
||||
return
|
||||
}
|
||||
// 将订单状态同步到 WooCommerce,然后切换至下一状态
|
||||
|
|
@ -278,6 +282,11 @@ export class OrderService {
|
|||
console.error('更新订单状态失败,原因为:', error)
|
||||
}
|
||||
}
|
||||
async getOrderByExternalOrderId(siteId: number, externalOrderId: string) {
|
||||
return await this.orderModel.findOne({
|
||||
where: { externalOrderId: String(externalOrderId), siteId },
|
||||
});
|
||||
}
|
||||
/**
|
||||
* 同步单个订单
|
||||
* 流程说明:
|
||||
|
|
@ -301,7 +310,7 @@ export class OrderService {
|
|||
* @param order 订单数据
|
||||
* @param forceUpdate 是否强制更新
|
||||
*/
|
||||
async syncSingleOrder(siteId: number, order: any, forceUpdate = false) {
|
||||
async syncSingleOrder(siteId: number, order: UnifiedOrderDTO, forceUpdate = false) {
|
||||
// 从订单数据中解构出各个子项
|
||||
let {
|
||||
line_items,
|
||||
|
|
@ -315,53 +324,50 @@ export class OrderService {
|
|||
// console.log('同步进单个订单', order)
|
||||
// 如果订单状态为 AUTO_DRAFT,则跳过处理
|
||||
if (order.status === OrderStatus.AUTO_DRAFT) {
|
||||
this.logger.debug('订单状态为 AUTO_DRAFT,跳过处理', siteId, order.id)
|
||||
return;
|
||||
}
|
||||
// 检查数据库中是否已存在该订单
|
||||
const existingOrder = await this.orderModel.findOne({
|
||||
where: { externalOrderId: order.id, siteId: siteId },
|
||||
where: { externalOrderId: String(order.id), siteId: siteId },
|
||||
});
|
||||
// 自动更新订单状态(如果需要)
|
||||
await this.autoUpdateOrderStatus(siteId, order);
|
||||
|
||||
if(existingOrder){
|
||||
// 矫正数据库中的订单数据
|
||||
const updateData: any = { status: order.status };
|
||||
if (this.canUpdateErpStatus(existingOrder.orderStatus)) {
|
||||
updateData.orderStatus = this.mapOrderStatus(order.status);
|
||||
}
|
||||
// 更新
|
||||
await this.orderModel.update({ externalOrderId: order.id, siteId: siteId }, updateData);
|
||||
// 更新 fulfillments 数据
|
||||
await this.saveOrderFulfillments({
|
||||
siteId,
|
||||
orderId: existingOrder.id,
|
||||
externalOrderId:order.id,
|
||||
fulfillments: fulfillments,
|
||||
});
|
||||
if (existingOrder) {
|
||||
// 矫正数据库中的订单数据
|
||||
const updateData: any = { status: order.status };
|
||||
if (this.canUpdateErpStatus(existingOrder.orderStatus)) {
|
||||
updateData.orderStatus = this.mapOrderStatus(order.status as any);
|
||||
}
|
||||
// 更新订单主数据
|
||||
await this.orderModel.update({ externalOrderId: String(order.id), siteId: siteId }, updateData);
|
||||
// 更新 fulfillments 数据
|
||||
await this.saveOrderFulfillments({
|
||||
siteId,
|
||||
orderId: existingOrder.id,
|
||||
externalOrderId: order.id,
|
||||
fulfillments: fulfillments,
|
||||
});
|
||||
}
|
||||
const externalOrderId = order.id;
|
||||
// 如果订单从未完成变为完成状态,则更新库存
|
||||
const externalOrderId = String(order.id);
|
||||
// 这里的 saveOrder 已经包括了创建订单和更新订单
|
||||
let orderRecord: Order = await this.saveOrder(siteId, orderData);
|
||||
// 如果订单从未完成变为完成状态,则更新库存
|
||||
if (
|
||||
existingOrder &&
|
||||
existingOrder.orderStatus !== ErpOrderStatus.COMPLETED &&
|
||||
orderRecord &&
|
||||
orderRecord.orderStatus !== ErpOrderStatus.COMPLETED &&
|
||||
orderData.status === OrderStatus.COMPLETED
|
||||
) {
|
||||
this.updateStock(existingOrder);
|
||||
await this.updateStock(orderRecord);
|
||||
// 不再直接返回,继续执行后续的更新操作
|
||||
}
|
||||
// 如果订单不可编辑且不强制更新,则跳过处理
|
||||
if (existingOrder && !existingOrder.is_editable && !forceUpdate) {
|
||||
return;
|
||||
}
|
||||
// 保存订单主数据
|
||||
const orderRecord = await this.saveOrder(siteId, orderData);
|
||||
const orderId = orderRecord.id;
|
||||
// 保存订单项
|
||||
await this.saveOrderItems({
|
||||
siteId,
|
||||
orderId,
|
||||
externalOrderId,
|
||||
externalOrderId: String(externalOrderId),
|
||||
orderItems: line_items,
|
||||
});
|
||||
// 保存退款信息
|
||||
|
|
@ -459,13 +465,14 @@ export class OrderService {
|
|||
* @param order 订单数据
|
||||
* @returns 保存后的订单实体
|
||||
*/
|
||||
async saveOrder(siteId: number, order: UnifiedOrderDTO): Promise<Order> {
|
||||
// 这里 omit 是因为处理在外头了 其实 saveOrder 应该包括 savelineitems 等
|
||||
async saveOrder(siteId: number, order: Omit<UnifiedOrderDTO, 'line_items' | 'refunds'>): Promise<Order> {
|
||||
// 将外部订单ID转换为字符串
|
||||
const externalOrderId = String(order.id)
|
||||
const externalOrderId = String(order.id)
|
||||
delete order.id
|
||||
|
||||
|
||||
// 创建订单实体对象
|
||||
const entity = plainToClass(Order, {...order, externalOrderId, siteId});
|
||||
const entity = plainToClass(Order, { ...order, externalOrderId, siteId });
|
||||
// 检查数据库中是否已存在该订单
|
||||
const existingOrder = await this.orderModel.findOne({
|
||||
where: { externalOrderId, siteId: siteId },
|
||||
|
|
@ -479,7 +486,7 @@ export class OrderService {
|
|||
// 如果不能更新 ERP 状态,则保留原有的 orderStatus
|
||||
entity.orderStatus = existingOrder.orderStatus;
|
||||
}
|
||||
// 更新订单数据(包括 shipping、billing 等字段)
|
||||
// 更新订单数据(包括 shipping、billing 等字段)
|
||||
await this.orderModel.update(existingOrder.id, entity);
|
||||
entity.id = existingOrder.id;
|
||||
return entity;
|
||||
|
|
@ -708,6 +715,8 @@ export class OrderService {
|
|||
*
|
||||
* @param orderItem 订单项实体
|
||||
*/
|
||||
// TODO 这里存的是库存商品实际
|
||||
// 所以叫做 orderInventoryItems 可能更合适
|
||||
async saveOrderSale(orderItem: OrderItem) {
|
||||
const currentOrderSale = await this.orderSaleModel.find({
|
||||
where: {
|
||||
|
|
@ -719,53 +728,56 @@ export class OrderService {
|
|||
await this.orderSaleModel.delete(currentOrderSale.map(v => v.id));
|
||||
}
|
||||
if (!orderItem.sku) return;
|
||||
|
||||
// 从数据库查询产品,关联查询组件
|
||||
const product = await this.productModel.findOne({
|
||||
where: { siteSkus: Like(`%${orderItem.sku}%`) },
|
||||
relations: ['components'],
|
||||
});
|
||||
const productDetail = await this.productService.getComponentDetailFromSiteSku({ sku: orderItem.sku, name: orderItem.name });
|
||||
|
||||
if (!product) return;
|
||||
|
||||
const orderSales: OrderSale[] = [];
|
||||
|
||||
if (product.components && product.components.length > 0) {
|
||||
for (const comp of product.components) {
|
||||
const baseProduct = await this.productModel.findOne({
|
||||
where: { sku: comp.sku },
|
||||
});
|
||||
if (baseProduct) {
|
||||
const orderSaleItem: OrderSale = plainToClass(OrderSale, {
|
||||
orderId: orderItem.orderId,
|
||||
siteId: orderItem.siteId,
|
||||
externalOrderItemId: orderItem.externalOrderItemId,
|
||||
productId: baseProduct.id,
|
||||
name: baseProduct.name,
|
||||
quantity: comp.quantity * orderItem.quantity,
|
||||
sku: comp.sku,
|
||||
isPackage: orderItem.name.toLowerCase().includes('package'),
|
||||
});
|
||||
orderSales.push(orderSaleItem);
|
||||
}
|
||||
if (!productDetail || !productDetail.quantity) return;
|
||||
const { product, quantity } = productDetail
|
||||
const componentDetails: { product: Product, quantity: number }[] = product.components?.length > 0 ? await Promise.all(product.components.map(async comp => {
|
||||
return {
|
||||
product: await this.productModel.findOne({
|
||||
where: { id: comp.productId },
|
||||
}),
|
||||
quantity: comp.quantity * orderItem.quantity,
|
||||
}
|
||||
} else {
|
||||
const orderSaleItem: OrderSale = plainToClass(OrderSale, {
|
||||
})) : [{ product, quantity }]
|
||||
|
||||
const orderSales: OrderSale[] = componentDetails.map(componentDetail => {
|
||||
if (!componentDetail.product) return null
|
||||
const attrsObj = this.productService.getAttributesObject(product.attributes)
|
||||
const orderSale = plainToClass(OrderSale, {
|
||||
orderId: orderItem.orderId,
|
||||
siteId: orderItem.siteId,
|
||||
externalOrderItemId: orderItem.externalOrderItemId,
|
||||
productId: product.id,
|
||||
name: product.name,
|
||||
quantity: orderItem.quantity,
|
||||
sku: product.sku,
|
||||
isPackage: orderItem.name.toLowerCase().includes('package'),
|
||||
externalOrderItemId: orderItem.externalOrderItemId,// 原始 itemId
|
||||
parentProductId: product.id, // 父产品 ID 用于统计套餐 如果是单品则不记录
|
||||
productId: componentDetail.product.id,
|
||||
isPackage: product.type === 'bundle',// 这里是否是套餐取决于父产品
|
||||
name: componentDetail.product.name,
|
||||
quantity: componentDetail.quantity * orderItem.quantity,
|
||||
sku: componentDetail.product.sku,
|
||||
// 理论上直接存 product 的全部数据才是对的,因为这样我的数据才全面。
|
||||
brand: attrsObj?.['brand']?.name,
|
||||
version: attrsObj?.['version']?.name,
|
||||
strength: attrsObj?.['strength']?.name,
|
||||
flavor: attrsObj?.['flavor']?.name,
|
||||
humidity: attrsObj?.['humidity']?.name,
|
||||
size: attrsObj?.['size']?.name,
|
||||
category: componentDetail.product.category.name,
|
||||
});
|
||||
orderSales.push(orderSaleItem);
|
||||
}
|
||||
|
||||
return orderSale
|
||||
}).filter(v => v !== null)
|
||||
if (orderSales.length > 0) {
|
||||
await this.orderSaleModel.save(orderSales);
|
||||
}
|
||||
}
|
||||
// // extract stren
|
||||
// extractNumberFromString(str: string): number {
|
||||
// if (!str) return 0;
|
||||
|
||||
// const num = parseInt(str, 10);
|
||||
// return isNaN(num) ? 0 : num;
|
||||
// }
|
||||
|
||||
/**
|
||||
* 保存订单退款信息
|
||||
|
|
@ -1234,13 +1246,13 @@ export class OrderService {
|
|||
parameters.push(siteId);
|
||||
}
|
||||
if (startDate) {
|
||||
sqlQuery += ` AND o.date_created >= ?`;
|
||||
totalQuery += ` AND o.date_created >= ?`;
|
||||
sqlQuery += ` AND o.date_paid >= ?`;
|
||||
totalQuery += ` AND o.date_paid >= ?`;
|
||||
parameters.push(startDate);
|
||||
}
|
||||
if (endDate) {
|
||||
sqlQuery += ` AND o.date_created <= ?`;
|
||||
totalQuery += ` AND o.date_created <= ?`;
|
||||
sqlQuery += ` AND o.date_paid <= ?`;
|
||||
totalQuery += ` AND o.date_paid <= ?`;
|
||||
parameters.push(endDate);
|
||||
}
|
||||
// 支付方式筛选(使用参数化,避免SQL注入)
|
||||
|
|
@ -1328,7 +1340,7 @@ export class OrderService {
|
|||
// 添加分页到主查询
|
||||
sqlQuery += `
|
||||
GROUP BY o.id
|
||||
ORDER BY o.date_created DESC
|
||||
ORDER BY o.date_paid DESC
|
||||
LIMIT ? OFFSET ?
|
||||
`;
|
||||
parameters.push(pageSize, (current - 1) * pageSize);
|
||||
|
|
@ -1426,7 +1438,7 @@ export class OrderService {
|
|||
* @param params 查询参数
|
||||
* @returns 销售统计和分页信息
|
||||
*/
|
||||
async getOrderSales({ siteId, startDate, endDate, current, pageSize, name, exceptPackage }: QueryOrderSalesDTO) {
|
||||
async getOrderSales({ siteId, startDate, endDate, current, pageSize, name, exceptPackage, orderBy }: QueryOrderSalesDTO) {
|
||||
const nameKeywords = name ? name.split(' ').filter(Boolean) : [];
|
||||
const defaultStart = dayjs().subtract(30, 'day').startOf('day').format('YYYY-MM-DD HH:mm:ss');
|
||||
const defaultEnd = dayjs().endOf('day').format('YYYY-MM-DD HH:mm:ss');
|
||||
|
|
@ -1467,7 +1479,7 @@ export class OrderService {
|
|||
}
|
||||
|
||||
let itemSql = `
|
||||
SELECT os.productId, os.name, SUM(os.quantity) AS totalQuantity, COUNT(DISTINCT os.orderId) AS totalOrders
|
||||
SELECT os.productId, os.name, os.sku, SUM(os.quantity) AS totalQuantity, COUNT(DISTINCT os.orderId) AS totalOrders
|
||||
FROM order_sale os
|
||||
INNER JOIN \`order\` o ON o.id = os.orderId
|
||||
WHERE o.date_paid BETWEEN ? AND ?
|
||||
|
|
@ -1489,7 +1501,7 @@ export class OrderService {
|
|||
}
|
||||
itemSql += nameCondition;
|
||||
itemSql += `
|
||||
GROUP BY os.productId, os.name
|
||||
GROUP BY os.productId, os.name, os.sku
|
||||
ORDER BY totalQuantity DESC
|
||||
LIMIT ? OFFSET ?
|
||||
`;
|
||||
|
|
@ -1546,7 +1558,6 @@ export class OrderService {
|
|||
GROUP BY os.productId
|
||||
`;
|
||||
|
||||
console.log('------3.5-----', pcSql, pcParams, exceptPackage);
|
||||
const pcResults = await this.orderSaleModel.query(pcSql, pcParams);
|
||||
|
||||
const pcMap = new Map<number, any>();
|
||||
|
|
@ -1579,14 +1590,14 @@ export class OrderService {
|
|||
`;
|
||||
let yooneSql = `
|
||||
SELECT
|
||||
SUM(CASE WHEN os.isYoone = 1 AND os.size = 3 THEN os.quantity ELSE 0 END) AS yoone3Quantity,
|
||||
SUM(CASE WHEN os.isYoone = 1 AND os.size = 6 THEN os.quantity ELSE 0 END) AS yoone6Quantity,
|
||||
SUM(CASE WHEN os.isYoone = 1 AND os.size = 9 THEN os.quantity ELSE 0 END) AS yoone9Quantity,
|
||||
SUM(CASE WHEN os.isYoone = 1 AND os.size = 12 THEN os.quantity ELSE 0 END) AS yoone12Quantity,
|
||||
SUM(CASE WHEN os.isYooneNew = 1 AND os.size = 12 THEN os.quantity ELSE 0 END) AS yoone12QuantityNew,
|
||||
SUM(CASE WHEN os.isYoone = 1 AND os.size = 15 THEN os.quantity ELSE 0 END) AS yoone15Quantity,
|
||||
SUM(CASE WHEN os.isYoone = 1 AND os.size = 18 THEN os.quantity ELSE 0 END) AS yoone18Quantity,
|
||||
SUM(CASE WHEN os.isZex = 1 THEN os.quantity ELSE 0 END) AS zexQuantity
|
||||
SUM(CASE WHEN os.brand = 'yoone' AND os.strength = '3mg' THEN os.quantity ELSE 0 END) AS yoone3Quantity,
|
||||
SUM(CASE WHEN os.brand = 'yoone' AND os.strength = '6mg' THEN os.quantity ELSE 0 END) AS yoone6Quantity,
|
||||
SUM(CASE WHEN os.brand = 'yoone' AND os.strength = '9mg' THEN os.quantity ELSE 0 END) AS yoone9Quantity,
|
||||
SUM(CASE WHEN os.brand = 'yoone' AND os.strength = '12mg' THEN os.quantity ELSE 0 END) AS yoone12Quantity,
|
||||
SUM(CASE WHEN os.brand = 'yoone' AND os.strength = '12mg' THEN os.quantity ELSE 0 END) AS yoone12QuantityNew,
|
||||
SUM(CASE WHEN os.brand = 'yoone' AND os.strength = '15mg' THEN os.quantity ELSE 0 END) AS yoone15Quantity,
|
||||
SUM(CASE WHEN os.brand = 'yoone' AND os.strength = '18mg' THEN os.quantity ELSE 0 END) AS yoone18Quantity,
|
||||
SUM(CASE WHEN os.brand = 'zex' THEN os.quantity ELSE 0 END) AS zexQuantity
|
||||
FROM order_sale os
|
||||
INNER JOIN \`order\` o ON o.id = os.orderId
|
||||
WHERE o.date_paid BETWEEN ? AND ?
|
||||
|
|
@ -1642,11 +1653,12 @@ export class OrderService {
|
|||
* @returns 订单项统计和分页信息
|
||||
*/
|
||||
async getOrderItems({
|
||||
current,
|
||||
pageSize,
|
||||
siteId,
|
||||
startDate,
|
||||
endDate,
|
||||
current,
|
||||
pageSize,
|
||||
sku,
|
||||
name,
|
||||
}: QueryOrderSalesDTO) {
|
||||
const nameKeywords = name ? name.split(' ').filter(Boolean) : [];
|
||||
|
|
@ -1904,8 +1916,8 @@ export class OrderService {
|
|||
const key = it?.externalSubscriptionId
|
||||
? `sub:${it.externalSubscriptionId}`
|
||||
: it?.externalOrderId
|
||||
? `ord:${it.externalOrderId}`
|
||||
: `id:${it?.id}`;
|
||||
? `ord:${it.externalOrderId}`
|
||||
: `id:${it?.id}`;
|
||||
if (!seen.has(key)) {
|
||||
seen.add(key);
|
||||
relatedList.push(it);
|
||||
|
|
@ -2199,14 +2211,14 @@ export class OrderService {
|
|||
for (const sale of sales) {
|
||||
const product = await productRepo.findOne({ where: { sku: sale.sku } });
|
||||
const saleItem = {
|
||||
orderId: order.id,
|
||||
siteId: order.siteId,
|
||||
externalOrderItemId: '-1',
|
||||
productId: product.id,
|
||||
name: product.name,
|
||||
sku: sale.sku,
|
||||
quantity: sale.quantity,
|
||||
};
|
||||
orderId: order.id,
|
||||
siteId: order.siteId,
|
||||
externalOrderItemId: '-1',
|
||||
productId: product.id,
|
||||
name: product.name,
|
||||
sku: sale.sku,
|
||||
quantity: sale.quantity,
|
||||
};
|
||||
await orderSaleRepo.save(saleItem);
|
||||
}
|
||||
});
|
||||
|
|
@ -2339,83 +2351,83 @@ export class OrderService {
|
|||
//换货功能更新OrderSale和Orderitem数据
|
||||
async updateExchangeOrder(orderId: number, data: any) {
|
||||
throw new Error('暂未实现')
|
||||
// try {
|
||||
// const dataSource = this.dataSourceManager.getDataSource('default');
|
||||
// let transactionError = undefined;
|
||||
// try {
|
||||
// const dataSource = this.dataSourceManager.getDataSource('default');
|
||||
// let transactionError = undefined;
|
||||
|
||||
// await dataSource.transaction(async manager => {
|
||||
// const orderRepo = manager.getRepository(Order);
|
||||
// const orderSaleRepo = manager.getRepository(OrderSale);
|
||||
// const orderItemRepo = manager.getRepository(OrderItem);
|
||||
// await dataSource.transaction(async manager => {
|
||||
// const orderRepo = manager.getRepository(Order);
|
||||
// const orderSaleRepo = manager.getRepository(OrderSale);
|
||||
// const orderItemRepo = manager.getRepository(OrderItem);
|
||||
|
||||
|
||||
// const productRepo = manager.getRepository(ProductV2);
|
||||
// const productRepo = manager.getRepository(ProductV2);
|
||||
|
||||
// const order = await orderRepo.findOneBy({ id: orderId });
|
||||
// let product: ProductV2;
|
||||
// const order = await orderRepo.findOneBy({ id: orderId });
|
||||
// let product: ProductV2;
|
||||
|
||||
// await orderSaleRepo.delete({ orderId });
|
||||
// await orderItemRepo.delete({ orderId });
|
||||
// for (const sale of data['sales']) {
|
||||
// product = await productRepo.findOneBy({ sku: sale['sku'] });
|
||||
// await orderSaleRepo.save({
|
||||
// orderId,
|
||||
// siteId: order.siteId,
|
||||
// productId: product.id,
|
||||
// name: product.name,
|
||||
// sku: sale['sku'],
|
||||
// quantity: sale['quantity'],
|
||||
// });
|
||||
// };
|
||||
// await orderSaleRepo.delete({ orderId });
|
||||
// await orderItemRepo.delete({ orderId });
|
||||
// for (const sale of data['sales']) {
|
||||
// product = await productRepo.findOneBy({ sku: sale['sku'] });
|
||||
// await orderSaleRepo.save({
|
||||
// orderId,
|
||||
// siteId: order.siteId,
|
||||
// productId: product.id,
|
||||
// name: product.name,
|
||||
// sku: sale['sku'],
|
||||
// quantity: sale['quantity'],
|
||||
// });
|
||||
// };
|
||||
|
||||
// for (const item of data['items']) {
|
||||
// product = await productRepo.findOneBy({ sku: item['sku'] });
|
||||
// for (const item of data['items']) {
|
||||
// product = await productRepo.findOneBy({ sku: item['sku'] });
|
||||
|
||||
// await orderItemRepo.save({
|
||||
// orderId,
|
||||
// siteId: order.siteId,
|
||||
// productId: product.id,
|
||||
// name: product.name,
|
||||
// externalOrderId: order.externalOrderId,
|
||||
// externalProductId: product.externalProductId,
|
||||
// await orderItemRepo.save({
|
||||
// orderId,
|
||||
// siteId: order.siteId,
|
||||
// productId: product.id,
|
||||
// name: product.name,
|
||||
// externalOrderId: order.externalOrderId,
|
||||
// externalProductId: product.externalProductId,
|
||||
|
||||
// sku: item['sku'],
|
||||
// quantity: item['quantity'],
|
||||
// });
|
||||
// sku: item['sku'],
|
||||
// quantity: item['quantity'],
|
||||
// });
|
||||
|
||||
// };
|
||||
// };
|
||||
|
||||
// //将是否换货状态改为true
|
||||
// await orderRepo.update(
|
||||
// order.id
|
||||
// , {
|
||||
// is_exchange: true
|
||||
// });
|
||||
// //将是否换货状态改为true
|
||||
// await orderRepo.update(
|
||||
// order.id
|
||||
// , {
|
||||
// is_exchange: true
|
||||
// });
|
||||
|
||||
// //查询这个用户换过多少次货
|
||||
// const counts = await orderRepo.countBy({
|
||||
// is_editable: true,
|
||||
// customer_email: order.customer_email,
|
||||
// });
|
||||
// //查询这个用户换过多少次货
|
||||
// const counts = await orderRepo.countBy({
|
||||
// is_editable: true,
|
||||
// customer_email: order.customer_email,
|
||||
// });
|
||||
|
||||
// //批量更新当前用户换货次数
|
||||
// await orderRepo.update({
|
||||
// customer_email: order.customer_email
|
||||
// }, {
|
||||
// exchange_frequency: counts
|
||||
// });
|
||||
// //批量更新当前用户换货次数
|
||||
// await orderRepo.update({
|
||||
// customer_email: order.customer_email
|
||||
// }, {
|
||||
// exchange_frequency: counts
|
||||
// });
|
||||
|
||||
// }).catch(error => {
|
||||
// transactionError = error;
|
||||
// });
|
||||
// }).catch(error => {
|
||||
// transactionError = error;
|
||||
// });
|
||||
|
||||
// if (transactionError !== undefined) {
|
||||
// throw new Error(`更新物流信息错误:${transactionError.message}`);
|
||||
// }
|
||||
// return true;
|
||||
// } catch (error) {
|
||||
// throw new Error(`更新发货产品失败:${error.message}`);
|
||||
// }
|
||||
// if (transactionError !== undefined) {
|
||||
// throw new Error(`更新物流信息错误:${transactionError.message}`);
|
||||
// }
|
||||
// return true;
|
||||
// } catch (error) {
|
||||
// throw new Error(`更新发货产品失败:${error.message}`);
|
||||
// }
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -2461,17 +2473,17 @@ export class OrderService {
|
|||
}
|
||||
|
||||
try {
|
||||
|
||||
|
||||
// 过滤掉NaN和非数字值,只保留有效的数字ID
|
||||
const validIds = ids?.filter?.(id => Number.isFinite(id) && id > 0);
|
||||
|
||||
|
||||
const dataSource = this.dataSourceManager.getDataSource('default');
|
||||
|
||||
|
||||
// 优化事务使用
|
||||
return await dataSource.transaction(async manager => {
|
||||
// 准备查询条件
|
||||
const whereCondition: any = {};
|
||||
if(validIds.length > 0){
|
||||
if (validIds.length > 0) {
|
||||
whereCondition.id = In(validIds);
|
||||
}
|
||||
|
||||
|
|
@ -2487,7 +2499,7 @@ export class OrderService {
|
|||
|
||||
// 获取所有订单ID
|
||||
const orderIds = orders.map(order => order.id);
|
||||
|
||||
|
||||
// 获取所有订单项
|
||||
const orderItems = await manager.getRepository(OrderItem).find({
|
||||
where: {
|
||||
|
|
@ -2508,13 +2520,13 @@ export class OrderService {
|
|||
const exportDataList: ExportData[] = orders.map(order => {
|
||||
// 获取订单的订单项
|
||||
const items = orderItemsByOrderId[order.id] || [];
|
||||
|
||||
|
||||
// 计算总盒数
|
||||
const boxCount = items.reduce((total, item) => total + item.quantity, 0);
|
||||
|
||||
|
||||
// 构建订单内容
|
||||
const orderContent = items.map(item => `${item.name} (${item.sku || ''}) x ${item.quantity}`).join('; ');
|
||||
|
||||
const orderContent = items.map(item => `${item.name} x ${item.quantity}`).join('; ');
|
||||
|
||||
// 构建姓名地址
|
||||
const shipping = order.shipping;
|
||||
const billing = order.billing;
|
||||
|
|
@ -2528,10 +2540,10 @@ export class OrderService {
|
|||
const postcode = shipping?.postcode || billing?.postcode || '';
|
||||
const country = shipping?.country || billing?.country || '';
|
||||
const nameAddress = `${name} ${address} ${address2} ${city} ${state} ${postcode} ${country}`;
|
||||
|
||||
|
||||
// 获取电话号码
|
||||
const phone = shipping?.phone || billing?.phone || '';
|
||||
|
||||
|
||||
// 获取快递号
|
||||
const trackingNumber = order.shipment?.tracking_id || '';
|
||||
|
||||
|
|
@ -2567,85 +2579,153 @@ export class OrderService {
|
|||
* 导出数据为CSV格式
|
||||
* @param {any[]} data 数据数组
|
||||
* @param {Object} options 配置选项
|
||||
* @param {string} [options.type='string'] 输出类型:'string' | 'buffer'
|
||||
* @param {string} [options.fileName] 文件名(仅当需要写入文件时使用)
|
||||
* @param {string} [options.type='string'] 输出类型:'string' | 'buffer'
|
||||
* @param {string} [options.fileName] 文件名(仅当需要写入文件时使用)
|
||||
* @param {boolean} [options.writeFile=false] 是否写入文件
|
||||
* @returns {string|Buffer} 根据type返回字符串或Buffer
|
||||
*/
|
||||
async exportToCsv(data: any[], options: { type?: 'string' | 'buffer'; fileName?: string; writeFile?: boolean } = {}): Promise<string | Buffer> {
|
||||
try {
|
||||
// 检查数据是否为空
|
||||
if (!data || data.length === 0) {
|
||||
throw new Error('导出数据不能为空');
|
||||
}
|
||||
|
||||
const { type = 'string', fileName, writeFile = false } = options;
|
||||
|
||||
// 生成表头
|
||||
const headers = Object.keys(data[0]);
|
||||
let csvContent = headers.join(',') + '\n';
|
||||
|
||||
// 处理数据行
|
||||
data.forEach(item => {
|
||||
const row = headers.map(key => {
|
||||
const value = item[key as keyof any];
|
||||
// 处理特殊字符
|
||||
if (typeof value === 'string') {
|
||||
// 转义双引号,将"替换为""
|
||||
const escapedValue = value.replace(/"/g, '""');
|
||||
// 如果包含逗号或换行符,需要用双引号包裹
|
||||
if (escapedValue.includes(',') || escapedValue.includes('\n')) {
|
||||
return `"${escapedValue}"`;
|
||||
}
|
||||
return escapedValue;
|
||||
}
|
||||
// 处理日期类型
|
||||
if (value instanceof Date) {
|
||||
return value.toISOString();
|
||||
}
|
||||
// 处理undefined和null
|
||||
if (value === undefined || value === null) {
|
||||
return '';
|
||||
}
|
||||
return String(value);
|
||||
}).join(',');
|
||||
csvContent += row + '\n';
|
||||
});
|
||||
|
||||
// 如果需要写入文件
|
||||
if (writeFile && fileName) {
|
||||
// 获取当前用户目录
|
||||
const userHomeDir = os.homedir();
|
||||
|
||||
// 构建目标路径(下载目录)
|
||||
const downloadsDir = path.join(userHomeDir, 'Downloads');
|
||||
|
||||
// 确保下载目录存在
|
||||
if (!fs.existsSync(downloadsDir)) {
|
||||
fs.mkdirSync(downloadsDir, { recursive: true });
|
||||
async exportToCsv(data: any[], options: { type?: 'string' | 'buffer'; fileName?: string; writeFile?: boolean } = {}): Promise<string | Buffer> {
|
||||
try {
|
||||
// 检查数据是否为空
|
||||
if (!data || data.length === 0) {
|
||||
throw new Error('导出数据不能为空');
|
||||
}
|
||||
|
||||
const filePath = path.join(downloadsDir, fileName);
|
||||
|
||||
// 写入文件
|
||||
fs.writeFileSync(filePath, csvContent, 'utf8');
|
||||
|
||||
console.log(`数据已成功导出至 ${filePath}`);
|
||||
return filePath;
|
||||
|
||||
const { type = 'string', fileName, writeFile = false } = options;
|
||||
|
||||
// 生成表头
|
||||
const headers = Object.keys(data[0]);
|
||||
let csvContent = headers.join(',') + '\n';
|
||||
|
||||
// 处理数据行
|
||||
data.forEach(item => {
|
||||
const row = headers.map(key => {
|
||||
const value = item[key as keyof any];
|
||||
// 处理特殊字符
|
||||
if (typeof value === 'string') {
|
||||
// 转义双引号,将"替换为""
|
||||
const escapedValue = value.replace(/"/g, '""');
|
||||
// 如果包含逗号或换行符,需要用双引号包裹
|
||||
if (escapedValue.includes(',') || escapedValue.includes('\n')) {
|
||||
return `"${escapedValue}"`;
|
||||
}
|
||||
return escapedValue;
|
||||
}
|
||||
// 处理日期类型
|
||||
if (value instanceof Date) {
|
||||
return value.toISOString();
|
||||
}
|
||||
// 处理undefined和null
|
||||
if (value === undefined || value === null) {
|
||||
return '';
|
||||
}
|
||||
return String(value);
|
||||
}).join(',');
|
||||
csvContent += row + '\n';
|
||||
});
|
||||
|
||||
// 如果需要写入文件
|
||||
if (writeFile && fileName) {
|
||||
// 获取当前用户目录
|
||||
const userHomeDir = os.homedir();
|
||||
|
||||
// 构建目标路径(下载目录)
|
||||
const downloadsDir = path.join(userHomeDir, 'Downloads');
|
||||
|
||||
// 确保下载目录存在
|
||||
if (!fs.existsSync(downloadsDir)) {
|
||||
fs.mkdirSync(downloadsDir, { recursive: true });
|
||||
}
|
||||
const filePath = path.join(downloadsDir, fileName);
|
||||
// 写入文件
|
||||
fs.writeFileSync(filePath, csvContent, 'utf8');
|
||||
return filePath;
|
||||
}
|
||||
|
||||
// 根据类型返回不同结果
|
||||
if (type === 'buffer') {
|
||||
return Buffer.from(csvContent, 'utf8');
|
||||
}
|
||||
|
||||
return csvContent;
|
||||
} catch (error) {
|
||||
throw new Error(`导出CSV文件失败: ${error.message}`);
|
||||
}
|
||||
|
||||
// 根据类型返回不同结果
|
||||
if (type === 'buffer') {
|
||||
return Buffer.from(csvContent, 'utf8');
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除每个分号前面一个左右括号和最后一个左右括号包含的内容(包括括号本身)
|
||||
* @param str 输入字符串
|
||||
* @returns 删除后的字符串
|
||||
*/
|
||||
removeLastParenthesesContent(str: string): string {
|
||||
if (!str || typeof str !== 'string') {
|
||||
return str;
|
||||
}
|
||||
|
||||
return csvContent;
|
||||
} catch (error) {
|
||||
console.error('导出CSV时出错:', error);
|
||||
throw new Error(`导出CSV文件失败: ${error.message}`);
|
||||
|
||||
// 辅助函数:删除指定位置的括号对及其内容
|
||||
const removeParenthesesAt = (s: string, leftIndex: number): string => {
|
||||
if (leftIndex === -1) return s;
|
||||
|
||||
let rightIndex = -1;
|
||||
let parenCount = 0;
|
||||
|
||||
for (let i = leftIndex; i < s.length; i++) {
|
||||
const char = s[i];
|
||||
if (char === '(') {
|
||||
parenCount++;
|
||||
} else if (char === ')') {
|
||||
parenCount--;
|
||||
if (parenCount === 0) {
|
||||
rightIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (rightIndex !== -1) {
|
||||
return s.substring(0, leftIndex) + s.substring(rightIndex + 1);
|
||||
}
|
||||
|
||||
return s;
|
||||
};
|
||||
|
||||
// 1. 处理每个分号前面的括号对
|
||||
let result = str;
|
||||
|
||||
// 找出所有分号的位置
|
||||
const semicolonIndices: number[] = [];
|
||||
for (let i = 0; i < result.length; i++) {
|
||||
if (result[i] === ';') {
|
||||
semicolonIndices.push(i);
|
||||
}
|
||||
}
|
||||
|
||||
// 从后向前处理每个分号,避免位置变化影响后续处理
|
||||
for (let i = semicolonIndices.length - 1; i >= 0; i--) {
|
||||
const semicolonIndex = semicolonIndices[i];
|
||||
|
||||
// 从分号位置向前查找最近的左括号
|
||||
let lastLeftParenIndex = -1;
|
||||
for (let j = semicolonIndex - 1; j >= 0; j--) {
|
||||
if (result[j] === '(') {
|
||||
lastLeftParenIndex = j;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 如果找到左括号,删除该括号对及其内容
|
||||
if (lastLeftParenIndex !== -1) {
|
||||
result = removeParenthesesAt(result, lastLeftParenIndex);
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 处理整个字符串的最后一个括号对
|
||||
let lastLeftParenIndex = result.lastIndexOf('(');
|
||||
if (lastLeftParenIndex !== -1) {
|
||||
result = removeParenthesesAt(result, lastLeftParenIndex);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -1,3 +1,6 @@
|
|||
/**
|
||||
* https://www.apizza.net/project/e114fb8e628e0f604379f5b26f0d8330/browse
|
||||
*/
|
||||
import { ILogger, Inject, Provide } from '@midwayjs/core';
|
||||
import axios, { AxiosRequestConfig } from 'axios';
|
||||
import * as fs from 'fs';
|
||||
|
|
@ -5,16 +8,16 @@ import * as FormData from 'form-data';
|
|||
import { SiteService } from './site.service';
|
||||
import { Site } from '../entity/site.entity';
|
||||
import { UnifiedReviewDTO } from '../dto/site-api.dto';
|
||||
import { ShopyyReview } from '../dto/shopyy.dto';
|
||||
import { ShopyyGetOneOrderResult, ShopyyReview } from '../dto/shopyy.dto';
|
||||
import { BatchOperationDTO, BatchOperationResultDTO } from '../dto/batch.dto';
|
||||
import { UnifiedSearchParamsDTO } from '../dto/api.dto';
|
||||
import { UnifiedSearchParamsDTO, ShopyyGetAllOrdersParams } from '../dto/api.dto';
|
||||
/**
|
||||
* ShopYY平台服务实现
|
||||
*/
|
||||
@Provide()
|
||||
export class ShopyyService {
|
||||
@Inject()
|
||||
logger:ILogger;
|
||||
logger: ILogger;
|
||||
/**
|
||||
* 获取ShopYY评论列表
|
||||
* @param site 站点配置
|
||||
|
|
@ -125,7 +128,7 @@ export class ShopyyService {
|
|||
* @returns 完整URL
|
||||
*/
|
||||
private buildURL(baseUrl: string, endpoint: string): string {
|
||||
// ShopYY API URL格式:https://{shop}.shopyy.com/openapi/{version}/{endpoint}
|
||||
// ShopYY API URL格式:https://{shop}.shopyy.com/openapi/{version}/{endpoint}
|
||||
const base = baseUrl.replace(/\/$/, '');
|
||||
const end = endpoint.replace(/^\//, '');
|
||||
return `${base}/${end}`;
|
||||
|
|
@ -155,7 +158,7 @@ export class ShopyyService {
|
|||
* @param params 请求参数
|
||||
* @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 headers = this.buildHeaders(site);
|
||||
|
||||
|
|
@ -180,41 +183,19 @@ export class ShopyyService {
|
|||
* 通用分页获取资源
|
||||
*/
|
||||
public async fetchResourcePaged<T>(site: any, endpoint: string, params: Record<string, any> = {}) {
|
||||
const page = Number(params.page || 1);
|
||||
const limit = Number(params.per_page ?? 20);
|
||||
const where = params.where && typeof params.where === 'object' ? params.where : {};
|
||||
let orderby: string | undefined = params.orderby;
|
||||
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);
|
||||
const response = await this.request(site, endpoint, 'GET', null, params);
|
||||
return this.mapPageResponse<T>(response, params);
|
||||
}
|
||||
mapPageResponse<T>(response: any, query: Record<string, any>) {
|
||||
if (response?.code !== 0) {
|
||||
throw new Error(response?.msg)
|
||||
}
|
||||
|
||||
return {
|
||||
items: (response.data.list || []) as T[],
|
||||
total: response.data?.paginate?.total || 0,
|
||||
totalPages: response.data?.paginate?.pageTotal || 0,
|
||||
page: response.data?.paginate?.current || requestParams.page,
|
||||
per_page: response.data?.paginate?.pagesize || requestParams.limit,
|
||||
page: response.data?.paginate?.current || query.page,
|
||||
per_page: response.data?.paginate?.pagesize || query.limit,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -225,13 +206,13 @@ export class ShopyyService {
|
|||
* @param pageSize 每页数量
|
||||
* @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
|
||||
// 通过 fields 参数指定需要返回的字段,确保 handle 等关键信息被包含
|
||||
const response = await this.request(site, 'products', 'GET', null, {
|
||||
page,
|
||||
page_size: pageSize,
|
||||
fields: 'id,name,sku,handle,status,type,stock_status,stock_quantity,images,regular_price,sale_price,tags,variations'
|
||||
...where
|
||||
});
|
||||
|
||||
return {
|
||||
|
|
@ -291,7 +272,7 @@ export class ShopyyService {
|
|||
const response = await this.request(site, `products/${productId}/variations/${variationId}`, 'GET');
|
||||
return response.data;
|
||||
}
|
||||
mapOrderSearchParams(params: UnifiedSearchParamsDTO){
|
||||
mapOrderSearchParams(params: UnifiedSearchParamsDTO) {
|
||||
const { after, before, ...restParams } = params;
|
||||
return {
|
||||
...restParams,
|
||||
|
|
@ -307,7 +288,7 @@ export class ShopyyService {
|
|||
* @param pageSize 每页数量
|
||||
* @returns 分页订单列表
|
||||
*/
|
||||
async getOrders(site: any | number, page: number = 1, pageSize: number = 100, params: UnifiedSearchParamsDTO = {}): Promise<any> {
|
||||
async getOrders(site: any | number, page: number = 1, pageSize: number = 3000, params: ShopyyGetAllOrdersParams = {}): Promise<any> {
|
||||
// 如果传入的是站点ID,则获取站点配置
|
||||
const siteConfig = typeof site === 'number' ? await this.siteService.get(site) : site;
|
||||
|
||||
|
|
@ -327,12 +308,11 @@ export class ShopyyService {
|
|||
};
|
||||
}
|
||||
|
||||
async getAllOrders(site: any | number, params: Record<string, any> = {}, maxPages: number = 10, concurrencyLimit: number = 100): Promise<any> {
|
||||
const firstPage = await this.getOrders(site, 1, 100);
|
||||
|
||||
const { items: firstPageItems, totalPages} = firstPage;
|
||||
|
||||
// const { page = 1, per_page = 100 } = params;
|
||||
async getAllOrders(site: any | number, params: ShopyyGetAllOrdersParams = {}, maxPages: number = 10, concurrencyLimit: number = 100): Promise<any> {
|
||||
const firstPage = await this.getOrders(site, 1, 100, params);
|
||||
|
||||
const { items: firstPageItems, totalPages } = firstPage;
|
||||
|
||||
// 如果只有一页数据,直接返回
|
||||
if (totalPages <= 1) {
|
||||
return firstPageItems;
|
||||
|
|
@ -340,7 +320,7 @@ export class ShopyyService {
|
|||
|
||||
// 限制最大页数,避免过多的并发请求
|
||||
const actualMaxPages = Math.min(totalPages, maxPages);
|
||||
|
||||
|
||||
// 收集所有页面数据,从第二页开始
|
||||
const allItems = [...firstPageItems];
|
||||
let currentPage = 2;
|
||||
|
|
@ -349,35 +329,35 @@ export class ShopyyService {
|
|||
while (currentPage <= actualMaxPages) {
|
||||
const batchPromises: Promise<any[]>[] = [];
|
||||
const batchSize = Math.min(concurrencyLimit, actualMaxPages - currentPage + 1);
|
||||
|
||||
|
||||
// 创建当前批次的并发请求
|
||||
for (let i = 0; i < batchSize; i++) {
|
||||
const page = currentPage + i;
|
||||
const pagePromise = this.getOrders(site, page, 100)
|
||||
const pagePromise = this.getOrders(site, page, 100, params)
|
||||
.then(pageResult => pageResult.items)
|
||||
.catch(error => {
|
||||
console.error(`获取第 ${page} 页数据失败:`, error);
|
||||
return []; // 如果某页获取失败,返回空数组,不影响整体结果
|
||||
});
|
||||
|
||||
|
||||
batchPromises.push(pagePromise);
|
||||
}
|
||||
|
||||
// 等待当前批次完成
|
||||
const batchResults = await Promise.all(batchPromises);
|
||||
|
||||
|
||||
// 合并当前批次的数据
|
||||
for (const pageItems of batchResults) {
|
||||
allItems.push(...pageItems);
|
||||
}
|
||||
|
||||
|
||||
// 移动到下一批次
|
||||
currentPage += batchSize;
|
||||
}
|
||||
|
||||
return allItems;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 获取ShopYY订单详情
|
||||
|
|
@ -385,7 +365,7 @@ export class ShopyyService {
|
|||
* @param orderId 订单ID
|
||||
* @returns 订单详情
|
||||
*/
|
||||
async getOrder(siteId: string, orderId: string): Promise<any> {
|
||||
async getOrder(siteId: string, orderId: string): Promise<ShopyyGetOneOrderResult> {
|
||||
const site = await this.siteService.get(Number(siteId));
|
||||
|
||||
// ShopYY API: GET /orders/{id}
|
||||
|
|
@ -495,13 +475,16 @@ export class ShopyyService {
|
|||
async createFulfillment(site: Site, orderId: string, data: any): Promise<any> {
|
||||
// ShopYY API: POST /orders/{id}/shipments
|
||||
const fulfillmentData = {
|
||||
tracking_number: data.tracking_number,
|
||||
carrier_code: data.carrier_code,
|
||||
carrier_name: data.carrier_name,
|
||||
shipping_method: data.shipping_method
|
||||
data: [{
|
||||
order_number: orderId,
|
||||
tracking_company: data.tracking_company,
|
||||
tracking_number: data.tracking_number,
|
||||
carrier_code: data.carrier_code,
|
||||
note: "note",
|
||||
mode: ""
|
||||
}]
|
||||
};
|
||||
|
||||
const response = await this.request(site, `orders/${orderId}/shipments`, 'POST', fulfillmentData);
|
||||
const response = await this.request(site, `orders/fulfillments`, 'POST', fulfillmentData);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
|
|
@ -514,7 +497,7 @@ export class ShopyyService {
|
|||
*/
|
||||
async deleteFulfillment(site: any, orderId: string, fulfillmentId: string): Promise<boolean> {
|
||||
try {
|
||||
// ShopYY API: DELETE /orders/{order_id}/shipments/{fulfillment_id}
|
||||
// ShopYY API: DELETE /orders/fulfillments/{fulfillment_id}
|
||||
await this.request(site, `orders/${orderId}/fulfillments/${fulfillmentId}`, 'DELETE');
|
||||
return true;
|
||||
} catch (error) {
|
||||
|
|
@ -562,7 +545,7 @@ export class ShopyyService {
|
|||
try {
|
||||
// ShopYY API: PUT /orders/{order_id}/shipments/{tracking_id}
|
||||
const fulfillmentData: any = {};
|
||||
|
||||
|
||||
// 只传递有值的字段
|
||||
if (data.tracking_number !== undefined) {
|
||||
fulfillmentData.tracking_number = data.tracking_number;
|
||||
|
|
@ -665,10 +648,10 @@ export class ShopyyService {
|
|||
// ShopYY API: POST /products/batch
|
||||
const response = await this.request(site, 'products/batch', 'POST', data);
|
||||
const result = response.data;
|
||||
|
||||
|
||||
// 转换 ShopYY 批量操作结果为统一格式
|
||||
const errors: Array<{identifier: string, error: string}> = [];
|
||||
|
||||
const errors: Array<{ identifier: string, error: string }> = [];
|
||||
|
||||
// 假设 ShopYY 返回格式与 WooCommerce 类似: { create: [...], update: [...], delete: [...] }
|
||||
// 错误信息可能在每个项目的 error 字段中
|
||||
const checkForErrors = (items: any[]) => {
|
||||
|
|
@ -681,12 +664,12 @@ export class ShopyyService {
|
|||
}
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
// 检查每个操作类型的结果中的错误
|
||||
if (result.create) checkForErrors(result.create);
|
||||
if (result.update) checkForErrors(result.update);
|
||||
if (result.delete) checkForErrors(result.delete);
|
||||
|
||||
|
||||
return {
|
||||
total: (data.create?.length || 0) + (data.update?.length || 0) + (data.delete?.length || 0),
|
||||
processed: (result.create?.length || 0) + (result.update?.length || 0) + (result.delete?.length || 0),
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { Inject, Provide } from '@midwayjs/core';
|
||||
import { ILogger, Inject, Provide } from '@midwayjs/core';
|
||||
import { ShopyyAdapter } from '../adapter/shopyy.adapter';
|
||||
import { WooCommerceAdapter } from '../adapter/woocommerce.adapter';
|
||||
import { ISiteAdapter } from '../interface/site-adapter.interface';
|
||||
|
|
@ -7,6 +7,7 @@ import { SiteService } from './site.service';
|
|||
import { WPService } from './wp.service';
|
||||
import { ProductService } from './product.service';
|
||||
import { UnifiedProductDTO } from '../dto/site-api.dto';
|
||||
import { Product } from '../entity/product.entity';
|
||||
|
||||
@Provide()
|
||||
export class SiteApiService {
|
||||
|
|
@ -22,6 +23,9 @@ export class SiteApiService {
|
|||
@Inject()
|
||||
productService: ProductService;
|
||||
|
||||
@Inject()
|
||||
logger: ILogger;
|
||||
|
||||
async getAdapter(siteId: number): Promise<ISiteAdapter> {
|
||||
const site = await this.siteService.get(siteId, true);
|
||||
if (!site) {
|
||||
|
|
@ -39,7 +43,7 @@ export class SiteApiService {
|
|||
}
|
||||
return new ShopyyAdapter(site, this.shopyyService);
|
||||
}
|
||||
|
||||
|
||||
throw new Error(`Unsupported site type: ${site.type}`);
|
||||
}
|
||||
|
||||
|
|
@ -49,7 +53,7 @@ export class SiteApiService {
|
|||
* @param siteProduct 站点商品信息
|
||||
* @returns 包含ERP产品信息的站点商品
|
||||
*/
|
||||
async enrichSiteProductWithErpInfo(siteId: number, siteProduct: any): Promise<any> {
|
||||
async enrichSiteProductWithErpInfo(siteId: number, siteProduct: UnifiedProductDTO): Promise<UnifiedProductDTO & { erpProduct?: Product }> {
|
||||
if (!siteProduct || !siteProduct.sku) {
|
||||
return siteProduct;
|
||||
}
|
||||
|
|
@ -57,22 +61,11 @@ export class SiteApiService {
|
|||
try {
|
||||
// 使用站点SKU查询对应的ERP产品
|
||||
const erpProduct = await this.productService.findProductBySiteSku(siteProduct.sku);
|
||||
|
||||
|
||||
// 将ERP产品信息合并到站点商品中
|
||||
return {
|
||||
...siteProduct,
|
||||
erpProduct: {
|
||||
id: erpProduct.id,
|
||||
sku: erpProduct.sku,
|
||||
name: erpProduct.name,
|
||||
nameCn: erpProduct.nameCn,
|
||||
category: erpProduct.category,
|
||||
attributes: erpProduct.attributes,
|
||||
components: erpProduct.components,
|
||||
price: erpProduct.price,
|
||||
promotionPrice: erpProduct.promotionPrice,
|
||||
// 可以根据需要添加更多ERP产品字段
|
||||
}
|
||||
erpProduct,
|
||||
};
|
||||
} catch (error) {
|
||||
// 如果找不到对应的ERP产品,返回原始站点商品
|
||||
|
|
@ -87,7 +80,7 @@ export class SiteApiService {
|
|||
* @param siteProducts 站点商品列表
|
||||
* @returns 包含ERP产品信息的站点商品列表
|
||||
*/
|
||||
async enrichSiteProductsWithErpInfo(siteId: number, siteProducts: any[]): Promise<any[]> {
|
||||
async enrichSiteProductsWithErpInfo(siteId: number, siteProducts: UnifiedProductDTO[]): Promise<(UnifiedProductDTO & { erpProduct?: Product })[]> {
|
||||
if (!siteProducts || !siteProducts.length) {
|
||||
return siteProducts;
|
||||
}
|
||||
|
|
@ -108,38 +101,27 @@ export class SiteApiService {
|
|||
*/
|
||||
async upsertProduct(siteId: number, product: Partial<UnifiedProductDTO>): Promise<any> {
|
||||
const adapter = await this.getAdapter(siteId);
|
||||
|
||||
// 首先尝试查找产品
|
||||
if (product.id) {
|
||||
try {
|
||||
// 尝试获取产品以确认它是否存在
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
// 首先尝试查找产品
|
||||
if (!product.sku) {
|
||||
throw new Error('产品SKU不能为空');
|
||||
}
|
||||
// 尝试搜索具有相同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);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -160,7 +142,7 @@ export class SiteApiService {
|
|||
const result = await this.upsertProduct(siteId, product);
|
||||
// 判断是创建还是更新
|
||||
if (result && result.id) {
|
||||
// 简单判断:如果产品原本没有ID而现在有了,说明是创建的
|
||||
// 简单判断:如果产品原本没有ID而现在有了,说明是创建的
|
||||
if (!product.id || !product.id.toString().trim()) {
|
||||
results.created.push(result);
|
||||
} else {
|
||||
|
|
@ -189,17 +171,6 @@ export class SiteApiService {
|
|||
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
|
||||
|
|
|
|||
|
|
@ -15,8 +15,19 @@ export class StatisticsService {
|
|||
orderItemRepository: Repository<OrderItem>;
|
||||
|
||||
async getOrderStatistics(params: OrderStatisticsParams) {
|
||||
const { startDate, endDate, grouping, siteId } = params;
|
||||
const { startDate, endDate, grouping, siteId, country } = params;
|
||||
// const keywords = keyword ? keyword.split(' ').filter(Boolean) : [];
|
||||
|
||||
let siteIds = []
|
||||
if (country) {
|
||||
siteIds = await this.getSiteIds(country)
|
||||
}
|
||||
|
||||
if (siteId) {
|
||||
siteIds.push(siteId)
|
||||
}
|
||||
|
||||
|
||||
const start = dayjs(startDate).format('YYYY-MM-DD');
|
||||
const end = dayjs(endDate).add(1, 'd').format('YYYY-MM-DD');
|
||||
let sql
|
||||
|
|
@ -54,22 +65,24 @@ export class StatisticsService {
|
|||
AND o.status IN('processing','completed')
|
||||
`;
|
||||
if (siteId) sql += ` AND o.siteId=${siteId}`;
|
||||
if (siteIds.length) sql += ` AND o.siteId IN (${siteIds.join(',')})`;
|
||||
|
||||
sql += `
|
||||
GROUP BY o.id, o.date_paid, o.customer_email, o.total, o.source_type, o.siteId, o.utm_source
|
||||
),
|
||||
order_sales_summary AS (
|
||||
SELECT
|
||||
orderId,
|
||||
SUM(CASE WHEN name LIKE '%zyn%' THEN quantity ELSE 0 END) AS zyn_quantity,
|
||||
SUM(CASE WHEN name LIKE '%yoone%' THEN quantity ELSE 0 END) AS yoone_quantity,
|
||||
SUM(CASE WHEN name LIKE '%zex%' THEN quantity ELSE 0 END) AS zex_quantity,
|
||||
SUM(CASE WHEN name LIKE '%yoone%' AND isPackage = 1 THEN quantity ELSE 0 END) AS yoone_G_quantity,
|
||||
SUM(CASE WHEN name LIKE '%yoone%' AND isPackage = 0 THEN quantity ELSE 0 END) AS yoone_S_quantity,
|
||||
SUM(CASE WHEN name LIKE '%yoone%' AND name LIKE '%3%' THEN quantity ELSE 0 END) AS yoone_3_quantity,
|
||||
SUM(CASE WHEN name LIKE '%yoone%' AND name LIKE '%6%' THEN quantity ELSE 0 END) AS yoone_6_quantity,
|
||||
SUM(CASE WHEN name LIKE '%yoone%' AND name LIKE '%9%' THEN quantity ELSE 0 END) AS yoone_9_quantity,
|
||||
SUM(CASE WHEN name LIKE '%yoone%' AND name LIKE '%12%' THEN quantity ELSE 0 END) AS yoone_12_quantity,
|
||||
SUM(CASE WHEN name LIKE '%yoone%' AND name LIKE '%15%' THEN quantity ELSE 0 END) AS yoone_15_quantity
|
||||
SUM(CASE WHEN brand = 'zyn' THEN quantity ELSE 0 END) AS zyn_quantity,
|
||||
SUM(CASE WHEN brand = 'yoone' THEN quantity ELSE 0 END) AS yoone_quantity,
|
||||
SUM(CASE WHEN brand = 'zex' THEN quantity ELSE 0 END) AS zex_quantity,
|
||||
SUM(CASE WHEN brand = 'yoone' AND isPackage = 1 THEN quantity ELSE 0 END) AS yoone_G_quantity,
|
||||
SUM(CASE WHEN brand = 'yoone' AND isPackage = 0 THEN quantity ELSE 0 END) AS yoone_S_quantity,
|
||||
SUM(CASE WHEN brand = 'yoone' AND strength = '3mg' THEN quantity ELSE 0 END) AS yoone_3_quantity,
|
||||
SUM(CASE WHEN brand = 'yoone' AND strength = '6mg' THEN quantity ELSE 0 END) AS yoone_6_quantity,
|
||||
SUM(CASE WHEN brand = 'yoone' AND strength = '9mg' THEN quantity ELSE 0 END) AS yoone_9_quantity,
|
||||
SUM(CASE WHEN brand = 'yoone' AND strength = '12mg' THEN quantity ELSE 0 END) AS yoone_12_quantity,
|
||||
SUM(CASE WHEN brand = 'yoone' AND strength = '15mg' THEN quantity ELSE 0 END) AS yoone_15_quantity
|
||||
FROM order_sale
|
||||
GROUP BY orderId
|
||||
),
|
||||
|
|
@ -247,22 +260,25 @@ export class StatisticsService {
|
|||
LEFT JOIN order_item oi ON o.id = oi.orderId
|
||||
WHERE o.date_paid IS NOT NULL
|
||||
AND o.date_paid >= '${start}' AND o.date_paid < '${end}'
|
||||
AND o.status IN ('processing','completed')
|
||||
AND o.status IN ('processing','completed')`;
|
||||
if (siteId) sql += ` AND o.siteId=${siteId}`;
|
||||
if (siteIds.length) sql += ` AND o.siteId IN (${siteIds.join(',')})`;
|
||||
sql +=`
|
||||
GROUP BY o.id, o.date_paid, o.customer_email, o.total, o.source_type, o.siteId, o.utm_source
|
||||
),
|
||||
order_sales_summary AS (
|
||||
SELECT
|
||||
orderId,
|
||||
SUM(CASE WHEN name LIKE '%zyn%' THEN quantity ELSE 0 END) AS zyn_quantity,
|
||||
SUM(CASE WHEN name LIKE '%yoone%' THEN quantity ELSE 0 END) AS yoone_quantity,
|
||||
SUM(CASE WHEN name LIKE '%zex%' THEN quantity ELSE 0 END) AS zex_quantity,
|
||||
SUM(CASE WHEN name LIKE '%yoone%' AND isPackage = 1 THEN quantity ELSE 0 END) AS yoone_G_quantity,
|
||||
SUM(CASE WHEN name LIKE '%yoone%' AND isPackage = 0 THEN quantity ELSE 0 END) AS yoone_S_quantity,
|
||||
SUM(CASE WHEN name LIKE '%yoone%' AND name LIKE '%3%' THEN quantity ELSE 0 END) AS yoone_3_quantity,
|
||||
SUM(CASE WHEN name LIKE '%yoone%' AND name LIKE '%6%' THEN quantity ELSE 0 END) AS yoone_6_quantity,
|
||||
SUM(CASE WHEN name LIKE '%yoone%' AND name LIKE '%9%' THEN quantity ELSE 0 END) AS yoone_9_quantity,
|
||||
SUM(CASE WHEN name LIKE '%yoone%' AND name LIKE '%12%' THEN quantity ELSE 0 END) AS yoone_12_quantity,
|
||||
SUM(CASE WHEN name LIKE '%yoone%' AND name LIKE '%15%' THEN quantity ELSE 0 END) AS yoone_15_quantity
|
||||
SUM(CASE WHEN brand = 'zyn' THEN quantity ELSE 0 END) AS zyn_quantity,
|
||||
SUM(CASE WHEN brand = 'yoone' THEN quantity ELSE 0 END) AS yoone_quantity,
|
||||
SUM(CASE WHEN brand = 'zex' THEN quantity ELSE 0 END) AS zex_quantity,
|
||||
SUM(CASE WHEN brand = 'yoone' AND isPackage = 1 THEN quantity ELSE 0 END) AS yoone_G_quantity,
|
||||
SUM(CASE WHEN brand = 'yoone' AND isPackage = 0 THEN quantity ELSE 0 END) AS yoone_S_quantity,
|
||||
SUM(CASE WHEN brand = 'yoone' AND strength = '3mg' THEN quantity ELSE 0 END) AS yoone_3_quantity,
|
||||
SUM(CASE WHEN brand = 'yoone' AND strength = '6mg' THEN quantity ELSE 0 END) AS yoone_6_quantity,
|
||||
SUM(CASE WHEN brand = 'yoone' AND strength = '9mg' THEN quantity ELSE 0 END) AS yoone_9_quantity,
|
||||
SUM(CASE WHEN brand = 'yoone' AND strength = '12mg' THEN quantity ELSE 0 END) AS yoone_12_quantity,
|
||||
SUM(CASE WHEN brand = 'yoone' AND strength = '15mg' THEN quantity ELSE 0 END) AS yoone_15_quantity
|
||||
FROM order_sale
|
||||
GROUP BY orderId
|
||||
),
|
||||
|
|
@ -439,23 +455,27 @@ export class StatisticsService {
|
|||
LEFT JOIN first_order f ON o.customer_email = f.customer_email
|
||||
LEFT JOIN order_item oi ON o.id = oi.orderId
|
||||
WHERE o.date_paid IS NOT NULL
|
||||
AND o.date_paid >= '${start}' AND o.date_paid < '${end}'
|
||||
AND o.date_paid >= '${start}' AND o.date_paid < '${end}'
|
||||
`;
|
||||
if (siteId) sql += ` AND o.siteId=${siteId}`;
|
||||
if (siteIds.length) sql += ` AND o.siteId IN (${siteIds.join(',')})`;
|
||||
sql +=`
|
||||
AND o.status IN ('processing','completed')
|
||||
GROUP BY o.id, o.date_paid, o.customer_email, o.total, o.source_type, o.siteId, o.utm_source
|
||||
),
|
||||
order_sales_summary AS (
|
||||
SELECT
|
||||
orderId,
|
||||
SUM(CASE WHEN name LIKE '%zyn%' THEN quantity ELSE 0 END) AS zyn_quantity,
|
||||
SUM(CASE WHEN name LIKE '%yoone%' THEN quantity ELSE 0 END) AS yoone_quantity,
|
||||
SUM(CASE WHEN name LIKE '%zex%' THEN quantity ELSE 0 END) AS zex_quantity,
|
||||
SUM(CASE WHEN name LIKE '%yoone%' AND isPackage = 1 THEN quantity ELSE 0 END) AS yoone_G_quantity,
|
||||
SUM(CASE WHEN name LIKE '%yoone%' AND isPackage = 0 THEN quantity ELSE 0 END) AS yoone_S_quantity,
|
||||
SUM(CASE WHEN name LIKE '%yoone%' AND name LIKE '%3%' THEN quantity ELSE 0 END) AS yoone_3_quantity,
|
||||
SUM(CASE WHEN name LIKE '%yoone%' AND name LIKE '%6%' THEN quantity ELSE 0 END) AS yoone_6_quantity,
|
||||
SUM(CASE WHEN name LIKE '%yoone%' AND name LIKE '%9%' THEN quantity ELSE 0 END) AS yoone_9_quantity,
|
||||
SUM(CASE WHEN name LIKE '%yoone%' AND name LIKE '%12%' THEN quantity ELSE 0 END) AS yoone_12_quantity,
|
||||
SUM(CASE WHEN name LIKE '%yoone%' AND name LIKE '%15%' THEN quantity ELSE 0 END) AS yoone_15_quantity
|
||||
SUM(CASE WHEN brand = 'zyn' THEN quantity ELSE 0 END) AS zyn_quantity,
|
||||
SUM(CASE WHEN brand = 'yoone' THEN quantity ELSE 0 END) AS yoone_quantity,
|
||||
SUM(CASE WHEN brand = 'zex' THEN quantity ELSE 0 END) AS zex_quantity,
|
||||
SUM(CASE WHEN brand = 'yoone' AND isPackage = 1 THEN quantity ELSE 0 END) AS yoone_G_quantity,
|
||||
SUM(CASE WHEN brand = 'yoone' AND isPackage = 0 THEN quantity ELSE 0 END) AS yoone_S_quantity,
|
||||
SUM(CASE WHEN brand = 'yoone' AND strength = '3mg' THEN quantity ELSE 0 END) AS yoone_3_quantity,
|
||||
SUM(CASE WHEN brand = 'yoone' AND strength = '6mg' THEN quantity ELSE 0 END) AS yoone_6_quantity,
|
||||
SUM(CASE WHEN brand = 'yoone' AND strength = '9mg' THEN quantity ELSE 0 END) AS yoone_9_quantity,
|
||||
SUM(CASE WHEN brand = 'yoone' AND strength = '12mg' THEN quantity ELSE 0 END) AS yoone_12_quantity,
|
||||
SUM(CASE WHEN brand = 'yoone' AND strength = '15mg' THEN quantity ELSE 0 END) AS yoone_15_quantity
|
||||
FROM order_sale
|
||||
GROUP BY orderId
|
||||
),
|
||||
|
|
@ -1314,7 +1334,14 @@ export class StatisticsService {
|
|||
}
|
||||
|
||||
async getOrderSorce(params) {
|
||||
const sql = `
|
||||
const { country } = params;
|
||||
|
||||
let siteIds = []
|
||||
if (country) {
|
||||
siteIds = await this.getSiteIds(country)
|
||||
}
|
||||
|
||||
let sql = `
|
||||
WITH cutoff_months AS (
|
||||
SELECT
|
||||
DATE_FORMAT(DATE_SUB(CURDATE(), INTERVAL 7 MONTH), '%Y-%m') AS start_month,
|
||||
|
|
@ -1326,7 +1353,10 @@ export class StatisticsService {
|
|||
DATE_FORMAT(MIN(date_paid), '%Y-%m') AS first_order_month,
|
||||
SUM(total) AS first_order_total
|
||||
FROM \`order\`
|
||||
WHERE status IN ('processing', 'completed')
|
||||
WHERE status IN ('processing', 'completed')`;
|
||||
if (siteIds.length!=0) sql += ` AND siteId IN ('${siteIds.join("','")}')`;
|
||||
else sql += ` AND siteId IS NULL `;
|
||||
sql += `
|
||||
GROUP BY customer_email
|
||||
),
|
||||
order_months AS (
|
||||
|
|
@ -1334,7 +1364,10 @@ export class StatisticsService {
|
|||
customer_email,
|
||||
DATE_FORMAT(date_paid, '%Y-%m') AS order_month
|
||||
FROM \`order\`
|
||||
WHERE status IN ('processing', 'completed')
|
||||
WHERE status IN ('processing', 'completed')`;
|
||||
if (siteIds.length!=0) sql += ` AND siteId IN ('${siteIds.join("','")}')`;
|
||||
else sql += ` AND siteId IS NULL `;
|
||||
sql += `
|
||||
),
|
||||
filtered_orders AS (
|
||||
SELECT o.customer_email, o.order_month, u.first_order_month,u.first_order_total, c.start_month
|
||||
|
|
@ -1366,7 +1399,7 @@ export class StatisticsService {
|
|||
ORDER BY order_month DESC, first_order_month_group
|
||||
`
|
||||
|
||||
const inactiveSql = `
|
||||
let inactiveSql = `
|
||||
WITH
|
||||
cutoff_months AS (
|
||||
SELECT
|
||||
|
|
@ -1381,7 +1414,10 @@ export class StatisticsService {
|
|||
date_paid,
|
||||
total
|
||||
FROM \`order\`
|
||||
WHERE status IN ('processing', 'completed')
|
||||
WHERE status IN ('processing', 'completed')`;
|
||||
if (siteIds.length!=0) inactiveSql += ` AND siteId IN ('${siteIds.join("','")}')`;
|
||||
else inactiveSql += ` AND siteId IS NULL `;
|
||||
inactiveSql += `
|
||||
),
|
||||
|
||||
filtered_users AS (
|
||||
|
|
@ -1524,4 +1560,13 @@ export class StatisticsService {
|
|||
|
||||
}
|
||||
|
||||
async getSiteIds(country: any[]) {
|
||||
const sql = `
|
||||
SELECT DISTINCT sa.siteId as site_id FROM area a left join site_areas_area sa on a.id = sa.areaId WHERE a.code IN ('${country.join("','")}')
|
||||
`
|
||||
const res = await this.orderRepository.query(sql)
|
||||
return res.map(item => item.site_id)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
/**
|
||||
*
|
||||
* wp 接口参考:
|
||||
* https://developer.wordpress.org/rest-api/reference/media/
|
||||
* woocommerce:
|
||||
*
|
||||
*/
|
||||
import { Inject, Provide } from '@midwayjs/core';
|
||||
import axios, { AxiosRequestConfig } from 'axios';
|
||||
|
|
@ -10,7 +12,7 @@ import { IPlatformService } from '../interface/platform.interface';
|
|||
import { BatchOperationDTO, BatchOperationResultDTO } from '../dto/batch.dto';
|
||||
import * as FormData from 'form-data';
|
||||
import * as fs from 'fs';
|
||||
import { WooProduct, WooVariation } from '../dto/woocommerce.dto';
|
||||
import { WooProduct, WooVariation, WpMediaGetListParams } from '../dto/woocommerce.dto';
|
||||
const MAX_PAGE_SIZE = 100;
|
||||
@Provide()
|
||||
export class WPService implements IPlatformService {
|
||||
|
|
@ -44,7 +46,7 @@ export class WPService implements IPlatformService {
|
|||
* @param site 站点配置
|
||||
* @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({
|
||||
url: site.apiUrl,
|
||||
consumerKey: site.consumerKey,
|
||||
|
|
@ -240,9 +242,11 @@ export class WPService implements IPlatformService {
|
|||
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');
|
||||
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> {
|
||||
|
|
@ -252,9 +256,9 @@ export class WPService implements IPlatformService {
|
|||
}
|
||||
|
||||
|
||||
// 导出 WooCommerce 产品为特殊CSV(平台特性)
|
||||
// 导出 WooCommerce 产品为特殊CSV(平台特性)
|
||||
async exportProductsCsvSpecial(site: any, page: number = 1, pageSize: number = 100): Promise<string> {
|
||||
const list = await this.getProducts(site, page, 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 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');
|
||||
|
|
@ -1042,20 +1046,7 @@ export class WPService implements IPlatformService {
|
|||
};
|
||||
}
|
||||
|
||||
public async fetchMediaPaged(site: any, params: Record<string, any> = {}) {
|
||||
const page = Number(params.page ?? 1);
|
||||
const per_page = Number( params.per_page ?? 20);
|
||||
const where = params.where && typeof params.where === 'object' ? params.where : {};
|
||||
let orderby: string | undefined = params.orderby;
|
||||
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';
|
||||
}
|
||||
}
|
||||
public async fetchMediaPaged(site: any, params: Partial<WpMediaGetListParams> = {}) {
|
||||
const apiUrl = site.apiUrl;
|
||||
const { consumerKey, consumerSecret } = site as any;
|
||||
const endpoint = 'wp/v2/media';
|
||||
|
|
@ -1064,17 +1055,21 @@ export class WPService implements IPlatformService {
|
|||
const response = await axios.get(url, {
|
||||
headers: { Authorization: `Basic ${auth}` },
|
||||
params: {
|
||||
...where,
|
||||
...(params.search ? { search: params.search } : {}),
|
||||
...(orderby ? { orderby } : {}),
|
||||
...(order ? { order } : {}),
|
||||
page,
|
||||
per_page
|
||||
...params,
|
||||
page: params.page ?? 1,
|
||||
per_page: params.per_page ?? 20,
|
||||
}
|
||||
});
|
||||
// 检查是否有错误信息
|
||||
if(response?.data?.message){
|
||||
throw new Error(`获取${apiUrl}条媒体文件失败,原因为${response.data.message}`)
|
||||
}
|
||||
if(!Array.isArray(response.data)) {
|
||||
throw new Error(`获取${apiUrl}条媒体文件失败,原因为返回数据不是数组`);
|
||||
}
|
||||
const total = Number(response.headers['x-wp-total'] || 0);
|
||||
const totalPages = Number(response.headers['x-wp-totalpages'] || 0);
|
||||
return { items: response.data, total, totalPages, page, per_page, page_size: per_page };
|
||||
return { items: response.data, total, totalPages, page:params.page ?? 1, per_page: params.per_page ?? 20, page_size: params.per_page ?? 20 };
|
||||
}
|
||||
/**
|
||||
* 上传媒体文件
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
// 从 unified 到 数据库需要有个转换流程
|
||||
|
|
@ -0,0 +1 @@
|
|||
// 文件转换
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
import { UnifiedOrderDTO } from "../dto/site-api.dto";
|
||||
|
||||
export class ShipmentAdapter {
|
||||
// 用于导出物流需要的数据
|
||||
mapFromOrder(order: UnifiedOrderDTO): any {
|
||||
return order;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
export const toArray = (value: any): any[] => {
|
||||
if (Array.isArray(value)) return value;
|
||||
if (value === undefined || value === null) return [];
|
||||
return String(value).split(',').map(v => v.trim()).filter(Boolean);
|
||||
};
|
||||
|
||||
export const toNumber = (value: any): number | undefined => {
|
||||
if (value === undefined || value === null || value === '') return undefined;
|
||||
const n = Number(value);
|
||||
return Number.isFinite(n) ? n : undefined;
|
||||
};
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
// Test script for FreightwavesService createOrder method
|
||||
|
||||
const { FreightwavesService } = require('./dist/service/freightwaves.service');
|
||||
|
||||
async function testFreightwavesService() {
|
||||
try {
|
||||
// Create an instance of the FreightwavesService
|
||||
const service = new FreightwavesService();
|
||||
|
||||
// Call the test method
|
||||
console.log('Starting test for createOrder method...');
|
||||
const result = await service.testQueryOrder();
|
||||
|
||||
console.log('Test completed successfully!');
|
||||
console.log('Result:', result);
|
||||
console.log('\nTo run the actual createOrder request:');
|
||||
console.log('1. Uncomment the createOrder call in the testCreateOrder method');
|
||||
console.log('2. Update the test-secret, test-partner-id with real credentials');
|
||||
console.log('3. Run this script again');
|
||||
} catch (error) {
|
||||
console.error('Test failed:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Run the test
|
||||
testFreightwavesService();
|
||||
|
|
@ -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响应
|
||||
Loading…
Reference in New Issue