Compare commits

..

10 Commits

Author SHA1 Message Date
tikkhun 3664431931 feat(shopyy): 实现全量商品查询功能并优化产品相关逻辑
- 新增ShopyyAllProductQuery类支持全量商品查询参数
- 实现getAllProducts方法支持带条件查询
- 优化getProductBySku方法使用新查询接口
- 公开request方法便于子类调用
- 增加错误日志记录产品查找失败情况
- 修复产品permalink生成逻辑
2026-01-08 18:17:03 +08:00
tikkhun f797950b4c fix(sync_shipment): 捕获运单状态更新时的异常并记录日志
添加try-catch块来捕获updateShipmentState过程中可能出现的错误
使用logger记录错误信息以便后续排查
2026-01-08 16:08:41 +08:00
tikkhun cd0bcedfad feat: 增强产品同步功能并优化SKU生成逻辑
添加字典排序字段支持
优化产品同步流程,支持通过SKU同步
重构SKU模板生成逻辑,支持分类属性排序
完善产品导入导出功能,增加分类字段处理
统一产品操作方法,提升代码可维护性
2026-01-08 15:03:11 +08:00
tikkhun 93931f7915 Merge branch 'main' of https://git.yoone.ca/zksu/API 2026-01-07 20:38:46 +08:00
tikkhun 9e90d5f9cf refactor(api): 统一接口参数为对象形式并支持多条件查询
重构所有接口方法,将直接传递id参数改为接受where条件对象
支持通过id、sku、email等多条件查询实体
优化产品服务逻辑,支持通过sku直接查询产品
统一各适配器实现,确保接口一致性
2026-01-07 20:33:50 +08:00
tikkhun 6311451d61 refactor(interface): 重构站点适配器接口,按功能模块组织方法
重构 ISiteAdapter 接口,将相关方法按功能模块(客户、媒体、订单、产品等)分组
移除废弃的 fulfillOrder 方法
新增多个数据映射方法以支持统一数据格式转换
2026-01-07 20:33:50 +08:00
tikkhun 5e55b85107 feat(订单): 添加获取订单总数功能
实现订单总数统计接口,包括:
1. 在ISiteAdapter接口添加countOrders方法
2. 在WooCommerce和Shopyy适配器中实现该方法
3. 添加控制器端点暴露该功能
4. 优化订单查询参数映射逻辑

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

将通用的搜索参数映射逻辑提取为独立方法,提高代码复用性
2026-01-07 20:33:50 +08:00
tikkhun 89d7d78ccc refactor(api): 统一接口参数为对象形式并支持多条件查询
重构所有接口方法,将直接传递id参数改为接受where条件对象
支持通过id、sku、email等多条件查询实体
优化产品服务逻辑,支持通过sku直接查询产品
统一各适配器实现,确保接口一致性
2026-01-07 20:33:23 +08:00
tikkhun e024d8752d refactor(interface): 重构站点适配器接口,按功能模块组织方法
重构 ISiteAdapter 接口,将相关方法按功能模块(客户、媒体、订单、产品等)分组
移除废弃的 fulfillOrder 方法
新增多个数据映射方法以支持统一数据格式转换
2026-01-07 18:10:00 +08:00
tikkhun 8f6727ae75 feat(订单): 添加获取订单总数功能
实现订单总数统计接口,包括:
1. 在ISiteAdapter接口添加countOrders方法
2. 在WooCommerce和Shopyy适配器中实现该方法
3. 添加控制器端点暴露该功能
4. 优化订单查询参数映射逻辑

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

将通用的搜索参数映射逻辑提取为独立方法,提高代码复用性
2026-01-07 15:22:18 +08:00
50 changed files with 1499 additions and 3578 deletions

49
migration-guide.md Normal file
View File

@ -0,0 +1,49 @@
# 数据库迁移指南
为了支持区域坐标功能,需要执行数据库迁移操作,将新增的 `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开始为区域添加坐标信息

17
package-lock.json generated
View File

@ -523,23 +523,6 @@
"node": ">=18" "node": ">=18"
} }
}, },
"node_modules/@faker-js/faker": {
"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",
"url": "https://opencollective.com/fakerjs"
}
],
"license": "MIT",
"peer": true,
"engines": {
"node": "^20.19.0 || ^22.13.0 || ^23.5.0 || >=24.0.0",
"npm": ">=10"
}
},
"node_modules/@hapi/bourne": { "node_modules/@hapi/bourne": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmmirror.com/@hapi/bourne/-/bourne-3.0.0.tgz", "resolved": "https://registry.npmmirror.com/@hapi/bourne/-/bourne-3.0.0.tgz",

184
permutation_fix.md Normal file
View File

@ -0,0 +1,184 @@
# Permutation页面列表显示问题分析和修复方案
## 问题分析
经过代码分析,发现了以下几个可能导致列表不显示的问题:
### 1. API路径不匹配
前端代码中引用的API函数名与后端控制器中的路径不一致
- 前端:`productcontrollerGetcategoriesall`、`productcontrollerGetcategoryattributes`、`productcontrollerGetproductlist`
- 后端实际的API路径`/product/categories/all`、`/product/category/:id/attributes`、`/product/list`
### 2. 数据格式问题
- `getCategoryAttributes`返回的数据结构与前端期望的不匹配
- 属性值获取逻辑可能存在问题
### 3. 组合生成逻辑问题
- 在生成排列组合时,数据结构和键值对应可能不正确
## 修复方案
### 后端修复
1. **修改getCategoryAttributes方法** - 在`/Users/zksu/Developer/work/workcode/API/src/service/product.service.ts`中:
```typescript
// 获取分类下的属性配置
async getCategoryAttributes(categoryId: number): Promise<any[]> {
const category = await this.categoryModel.findOne({
where: { id: categoryId },
relations: ['attributes', 'attributes.attributeDict', 'attributes.attributeDict.items'],
});
if (!category) {
return [];
}
// 格式化返回,匹配前端期望的数据结构
return category.attributes.map(attr => ({
id: attr.id,
dictId: attr.attributeDict.id,
name: attr.attributeDict.name, // 用于generateKeyFromPermutation
title: attr.attributeDict.title, // 用于列标题
dict: {
id: attr.attributeDict.id,
name: attr.attributeDict.name,
title: attr.attributeDict.title,
items: attr.attributeDict.items || []
}
}));
}
```
2. **确保dict/items接口可用** - 检查字典项获取接口:
在`/Users/zksu/Developer/work/workcode/API/src/controller/dict.controller.ts`中添加或确认:
```typescript
@Get('/items')
async getDictItems(@Query('dictId') dictId: number) {
try {
const dict = await this.dictModel.findOne({
where: { id: dictId },
relations: ['items']
});
if (!dict) {
return [];
}
return dict.items || [];
} catch (error) {
return errorResponse(error?.message || error);
}
}
```
### 前端修复建议
1. **添加错误处理和调试信息**
```typescript
// 在获取属性值的地方添加错误处理
const fetchData = async () => {
setLoading(true);
try {
// 1. Fetch Attributes
const attrRes = await productcontrollerGetcategoryattributes({
id: categoryId,
});
console.log('Attributes response:', attrRes); // 调试用
const attrs = Array.isArray(attrRes) ? attrRes : attrRes?.data || [];
setAttributes(attrs);
// 2. Fetch Attribute Values (Dict Items)
const valuesMap: Record<string, any[]> = {};
for (const attr of attrs) {
const dictId = attr.dict?.id || attr.dictId;
if (dictId) {
try {
const itemsRes = await request('/dict/items', {
params: { dictId },
});
console.log(`Dict items for ${attr.name}:`, itemsRes); // 调试用
valuesMap[attr.name] = itemsRes || [];
} catch (error) {
console.error(`Failed to fetch items for dict ${dictId}:`, error);
valuesMap[attr.name] = [];
}
}
}
setAttributeValues(valuesMap);
// 3. Fetch Existing Products
await fetchProducts(categoryId);
} catch (error) {
console.error('Error in fetchData:', error);
message.error('获取数据失败');
} finally {
setLoading(false);
}
};
```
2. **修复组合生成逻辑**
```typescript
// 修改generateKeyFromPermutation函数
const generateKeyFromPermutation = (perm: any) => {
const parts = Object.keys(perm).map((attrName) => {
const valItem = perm[attrName];
const val = valItem.name || valItem.value; // 兼容不同的数据格式
return `${attrName}:${val}`;
});
return parts.sort().join('|');
};
// 修改generateAttributeKey函数
const generateAttributeKey = (attrs: any[]) => {
const parts = attrs.map((a) => {
const key = a.dict?.name || a.dictName || a.name;
const val = a.name || a.value;
return `${key}:${val}`;
});
return parts.sort().join('|');
};
```
3. **添加空状态处理**
```typescript
// 在ProTable中添加空状态提示
<ProTable
// ... 其他属性
locale={{
emptyText: permutations.length === 0 && !loading ? '暂无数据,请检查分类属性配置' : '暂无数据'
}}
/>
```
## 调试步骤
1. **检查网络请求**
- 打开浏览器开发者工具
- 检查 `/product/categories/all` 请求是否成功
- 检查 `/product/category/:id/attributes` 请求返回的数据格式
- 检查 `/dict/items?dictId=:id` 请求是否成功
- 检查 `/product/list` 请求是否成功
2. **检查控制台日志**
- 查看属性数据是否正确加载
- 查看属性值是否正确获取
- 查看排列组合是否正确生成
3. **检查数据结构**
- 确认 `attributes` 数组是否正确
- 确认 `attributeValues` 对象是否正确填充
- 确认 `permutations` 数组是否正确生成
## 测试验证
1. 选择一个有属性配置的分类
2. 确认属性有对应的字典项
3. 检查排列组合是否正确显示
4. 验证现有产品匹配是否正确

View File

@ -21,16 +21,13 @@ import {
CreateReviewDTO, CreateReviewDTO,
CreateVariationDTO, CreateVariationDTO,
UpdateReviewDTO, UpdateReviewDTO,
OrderPaymentStatus,
} from '../dto/site-api.dto'; } from '../dto/site-api.dto';
import { UnifiedPaginationDTO, UnifiedSearchParamsDTO, ShopyyGetAllOrdersParams } from '../dto/api.dto'; import { UnifiedPaginationDTO, UnifiedSearchParamsDTO, } from '../dto/api.dto';
import { import {
ShopyyAllProductQuery, ShopyyAllProductQuery,
ShopyyCustomer, ShopyyCustomer,
ShopyyOrder, ShopyyOrder,
ShopyyOrderCreateParams,
ShopyyOrderQuery, ShopyyOrderQuery,
ShopyyOrderUpdateParams,
ShopyyProduct, ShopyyProduct,
ShopyyProductQuery, ShopyyProductQuery,
ShopyyVariant, ShopyyVariant,
@ -40,17 +37,16 @@ import {
OrderStatus, OrderStatus,
} from '../enums/base.enum'; } from '../enums/base.enum';
import { BatchOperationDTO, BatchOperationResultDTO } from '../dto/batch.dto'; import { BatchOperationDTO, BatchOperationResultDTO } from '../dto/batch.dto';
import dayjs = require('dayjs');
export class ShopyyAdapter implements ISiteAdapter { export class ShopyyAdapter implements ISiteAdapter {
shopyyFinancialStatusMap = { shopyyFinancialStatusMap= {
'200': '待支付', '200': '待支付',
'210': "支付中", '210': "支付中",
'220': "部分支付", '220':"部分支付",
'230': "已支付", '230':"已支付",
'240': "支付失败", '240':"支付失败",
'250': "部分退款", '250':"部分退款",
'260': "已退款", '260':"已退款",
'290': "已取消", '290':"已取消",
} }
constructor(private site: any, private shopyyService: ShopyyService) { constructor(private site: any, private shopyyService: ShopyyService) {
this.mapPlatformToUnifiedCustomer = this.mapPlatformToUnifiedCustomer.bind(this); this.mapPlatformToUnifiedCustomer = this.mapPlatformToUnifiedCustomer.bind(this);
@ -127,8 +123,8 @@ export class ShopyyAdapter implements ISiteAdapter {
return data return data
} }
async getCustomer(where: { id?: string | number, email?: string, phone?: string }): Promise<UnifiedCustomerDTO> { async getCustomer(where: {id?: string | number,email?: string,phone?: string}): Promise<UnifiedCustomerDTO> {
if (!where.id && !where.email && !where.phone) { if(!where.id && !where.email && !where.phone){
throw new Error('必须传入 id 或 email 或 phone') throw new Error('必须传入 id 或 email 或 phone')
} }
const customer = await this.shopyyService.getCustomer(this.site, where.id); const customer = await this.shopyyService.getCustomer(this.site, where.id);
@ -157,12 +153,12 @@ export class ShopyyAdapter implements ISiteAdapter {
return this.mapPlatformToUnifiedCustomer(createdCustomer); return this.mapPlatformToUnifiedCustomer(createdCustomer);
} }
async updateCustomer(where: { id: string | number }, data: Partial<UnifiedCustomerDTO>): Promise<UnifiedCustomerDTO> { async updateCustomer(where: {id: string | number}, data: Partial<UnifiedCustomerDTO>): Promise<UnifiedCustomerDTO> {
const updatedCustomer = await this.shopyyService.updateCustomer(this.site, where.id, data); const updatedCustomer = await this.shopyyService.updateCustomer(this.site, where.id, data);
return this.mapPlatformToUnifiedCustomer(updatedCustomer); return this.mapPlatformToUnifiedCustomer(updatedCustomer);
} }
async deleteCustomer(where: { id: string | number }): Promise<boolean> { async deleteCustomer(where: {id: string | number}): Promise<boolean> {
return await this.shopyyService.deleteCustomer(this.site, where.id); return await this.shopyyService.deleteCustomer(this.site, where.id);
} }
@ -216,12 +212,12 @@ export class ShopyyAdapter implements ISiteAdapter {
return this.mapPlatformToUnifiedMedia(createdMedia); return this.mapPlatformToUnifiedMedia(createdMedia);
} }
async updateMedia(where: { id: string | number }, data: any): Promise<UnifiedMediaDTO> { async updateMedia(where: {id: string | number}, data: any): Promise<UnifiedMediaDTO> {
const updatedMedia = await this.shopyyService.updateMedia(this.site, where.id, data); const updatedMedia = await this.shopyyService.updateMedia(this.site, where.id, data);
return this.mapPlatformToUnifiedMedia(updatedMedia); return this.mapPlatformToUnifiedMedia(updatedMedia);
} }
async deleteMedia(where: { id: string | number }): Promise<boolean> { async deleteMedia(where: {id: string | number}): Promise<boolean> {
return await this.shopyyService.deleteMedia(this.site, where.id); return await this.shopyyService.deleteMedia(this.site, where.id);
} }
@ -231,11 +227,9 @@ export class ShopyyAdapter implements ISiteAdapter {
// ========== 订单映射方法 ========== // ========== 订单映射方法 ==========
mapPlatformToUnifiedOrder(item: ShopyyOrder): UnifiedOrderDTO { mapPlatformToUnifiedOrder(item: ShopyyOrder): UnifiedOrderDTO {
// console.log(item)
if (!item) throw new Error('订单数据不能为空')
// 提取账单和送货地址 如果不存在则为空对象 // 提取账单和送货地址 如果不存在则为空对象
const billing = item.billing_address || {}; const billing = (item as any).billing_address || {};
const shipping = item.shipping_address || {}; const shipping = (item as any).shipping_address || {};
// 构建账单地址对象 // 构建账单地址对象
const billingObj: UnifiedAddressDTO = { const billingObj: UnifiedAddressDTO = {
@ -244,7 +238,7 @@ export class ShopyyAdapter implements ISiteAdapter {
fullname: billing.name || `${item.firstname} ${item.lastname}`.trim(), fullname: billing.name || `${item.firstname} ${item.lastname}`.trim(),
company: billing.company || '', company: billing.company || '',
email: item.customer_email || item.email || '', email: item.customer_email || item.email || '',
phone: billing.phone || item.telephone || '', phone: billing.phone || (item as any).telephone || '',
address_1: billing.address1 || item.payment_address || '', address_1: billing.address1 || item.payment_address || '',
address_2: billing.address2 || '', address_2: billing.address2 || '',
city: billing.city || item.payment_city || '', city: billing.city || item.payment_city || '',
@ -275,7 +269,6 @@ export class ShopyyAdapter implements ISiteAdapter {
state: shipping.province || item.shipping_zone || '', state: shipping.province || item.shipping_zone || '',
postcode: shipping.zip || item.shipping_postcode || '', postcode: shipping.zip || item.shipping_postcode || '',
method_title: item.payment_method || '', method_title: item.payment_method || '',
phone: shipping.phone || item.telephone || '',
country: country:
shipping.country_name || shipping.country_name ||
shipping.country_code || shipping.country_code ||
@ -314,14 +307,14 @@ export class ShopyyAdapter implements ISiteAdapter {
}; };
const lineItems: UnifiedOrderLineItemDTO[] = (item.products || []).map( const lineItems: UnifiedOrderLineItemDTO[] = (item.products || []).map(
(product) => ({ (p: any) => ({
id: product.id, id: p.id,
name:product.sku_value?.[0]?.value || product.product_title || product.name, name: p.product_title || p.name,
product_id: product.product_id, product_id: p.product_id,
quantity: product.quantity, quantity: p.quantity,
total: String(product.price ?? ''), total: String(p.price ?? ''),
sku: product.sku || product.sku_code || '', sku: p.sku || p.sku_code || '',
price: String(product.price ?? ''), price: String(p.price ?? ''),
}) })
); );
// 货币符号 // 货币符号
@ -339,12 +332,12 @@ export class ShopyyAdapter implements ISiteAdapter {
'SGD': 'S$' 'SGD': 'S$'
// 可以根据需要添加更多货币代码和符号 // 可以根据需要添加更多货币代码和符号
}; };
// 映射订单状态,如果不存在则默认 pending // 映射订单状态,如果不存在则默认 pending
const status = this.shopyyOrderStatusMap[item.status ?? item.order_status] || OrderStatus.PENDING; const status = this.shopyyOrderStatusMap[item.status ?? item.order_status] || OrderStatus.PENDING;
const finalcial_status = this.shopyyFinancialStatusMap[item.financial_status] const finalcial_status = this.shopyyFinancialStatusMap[item.financial_status]
// 发货状态 // 发货状态
const fulfillment_status = this.fulfillmentStatusMap[item.fulfillment_status]; const fulfillment_status = this.shopyyFulfillmentStatusMap[item.fulfillment_status];
return { return {
id: item.id || item.order_id, id: item.id || item.order_id,
number: item.order_number || item.order_sn, number: item.order_number || item.order_sn,
@ -374,6 +367,7 @@ export class ShopyyAdapter implements ISiteAdapter {
date_paid: typeof item.pay_at === 'number' date_paid: typeof item.pay_at === 'number'
? item.pay_at === 0 ? null : new Date(item.pay_at * 1000).toISOString() ? item.pay_at === 0 ? null : new Date(item.pay_at * 1000).toISOString()
: null, : null,
refunds: [], refunds: [],
currency_symbol: (currencySymbols[item.currency] || '$') || '', currency_symbol: (currencySymbols[item.currency] || '$') || '',
date_created: date_created:
@ -393,7 +387,6 @@ export class ShopyyAdapter implements ISiteAdapter {
tracking_number: f.tracking_number || '', tracking_number: f.tracking_number || '',
shipping_provider: f.tracking_company || '', shipping_provider: f.tracking_company || '',
shipping_method: f.tracking_company || '', shipping_method: f.tracking_company || '',
date_created: typeof f.created_at === 'number' date_created: typeof f.created_at === 'number'
? new Date(f.created_at * 1000).toISOString() ? new Date(f.created_at * 1000).toISOString()
: f.created_at || '', : f.created_at || '',
@ -407,12 +400,12 @@ export class ShopyyAdapter implements ISiteAdapter {
return data return data
} }
mapCreateOrderParams(data: Partial<UnifiedOrderDTO>): ShopyyOrderCreateParams { mapCreateOrderParams(data: Partial<UnifiedOrderDTO>): any {
return data return data
} }
mapUpdateOrderParams(data: Partial<UnifiedOrderDTO>): ShopyyOrderUpdateParams { mapUpdateOrderParams(data: Partial<UnifiedOrderDTO>): any {
// 构建 ShopYY 订单更新参数(仅包含传入的字段) // 构建 ShopYY 订单更新参数(仅包含传入的字段)
const params: any = {}; const params: any = {};
// 仅当字段存在时才添加到更新参数中 // 仅当字段存在时才添加到更新参数中
@ -447,7 +440,7 @@ export class ShopyyAdapter implements ISiteAdapter {
} }
// 更新账单地址 // 更新账单地址
params.billing_address = params?.billing_address || {}; params.billing_address = params.billing_address || {};
if (data.billing.first_name !== undefined) { if (data.billing.first_name !== undefined) {
params.billing_address.first_name = data.billing.first_name; params.billing_address.first_name = data.billing.first_name;
} }
@ -541,17 +534,9 @@ export class ShopyyAdapter implements ISiteAdapter {
return params; return params;
} }
async getOrder(where: { id: string | number }): Promise<UnifiedOrderDTO> { async getOrder(where: {id: string | number}): Promise<UnifiedOrderDTO> {
const data = await this.getOrders({ const data = await this.shopyyService.getOrder(this.site.id, String(where.id));
where: { return this.mapPlatformToUnifiedOrder(data);
id: where.id,
},
page: 1,
per_page: 1,
})
return data.items[0] || null
// const data = await this.shopyyService.getOrder(this.site.id, String(where.id));
// return this.mapPlatformToUnifiedOrder(data);
} }
async getOrders( async getOrders(
@ -572,25 +557,13 @@ export class ShopyyAdapter implements ISiteAdapter {
per_page, per_page,
}; };
} }
mapGetAllOrdersParams(params: UnifiedSearchParamsDTO) :ShopyyGetAllOrdersParams{
const pay_at_min = dayjs(params.after || '').unix().toString();
const pay_at_max = dayjs(params.before || '').unix().toString();
return {
per_page: params.per_page || 100,
pay_at_min: pay_at_min,
pay_at_max: pay_at_max,
order_field: 'pay_at',
}
}
async getAllOrders(params?: UnifiedSearchParamsDTO): Promise<UnifiedOrderDTO[]> { async getAllOrders(params?: UnifiedSearchParamsDTO): Promise<UnifiedOrderDTO[]> {
const normalizedParams = this.mapGetAllOrdersParams(params); const data = await this.shopyyService.getAllOrders(this.site.id, params);
const data = await this.shopyyService.getAllOrders(this.site.id, normalizedParams);
return data.map(this.mapPlatformToUnifiedOrder.bind(this)); return data.map(this.mapPlatformToUnifiedOrder.bind(this));
} }
async countOrders(where: Record<string, any>): Promise<number> { async countOrders(where: Record<string,any>): Promise<number> {
// 使用最小分页只获取总数 // 使用最小分页只获取总数
const searchParams = { const searchParams = {
where, where,
@ -608,13 +581,13 @@ export class ShopyyAdapter implements ISiteAdapter {
return this.mapPlatformToUnifiedOrder(createdOrder); return this.mapPlatformToUnifiedOrder(createdOrder);
} }
async updateOrder(where: { id: string | number }, data: Partial<UnifiedOrderDTO>): Promise<boolean> { async updateOrder(where: {id: string | number}, data: Partial<UnifiedOrderDTO>): Promise<boolean> {
// 使用映射方法转换参数 // 使用映射方法转换参数
const requestParams = this.mapUpdateOrderParams(data); const requestParams = this.mapUpdateOrderParams(data);
return await this.shopyyService.updateOrder(this.site, String(where.id), requestParams); return await this.shopyyService.updateOrder(this.site, String(where.id), requestParams);
} }
async deleteOrder(where: { id: string | number }): Promise<boolean> { async deleteOrder(where: {id: string | number}): Promise<boolean> {
return await this.shopyyService.deleteOrder(this.site, where.id); return await this.shopyyService.deleteOrder(this.site, where.id);
} }
@ -687,15 +660,15 @@ export class ShopyyAdapter implements ISiteAdapter {
mapOrderSearchParams(params: UnifiedSearchParamsDTO): Partial<ShopyyOrderQuery> { mapOrderSearchParams(params: UnifiedSearchParamsDTO): Partial<ShopyyOrderQuery> {
// 首先使用通用参数转换 // 首先使用通用参数转换
const baseParams = this.mapSearchParams(params); const baseParams = this.mapSearchParams(params);
// 订单状态映射 // 订单状态映射
const statusMap = { const statusMap = {
'pending': '100', // 100 未完成 'pending': '100', // 100 未完成
'processing': '110', // 110 待处理 'processing': '110', // 110 待处理
'completed': "180", // 180 已完成(确认收货) 'completed': "180", // 180 已完成(确认收货)
'cancelled': '190', // 190 取消 'cancelled': '190', // 190 取消
}; };
// 如果有状态参数,进行特殊映射 // 如果有状态参数,进行特殊映射
if (baseParams.status) { if (baseParams.status) {
const unifiedStatus = baseParams.status const unifiedStatus = baseParams.status
@ -703,13 +676,13 @@ export class ShopyyAdapter implements ISiteAdapter {
baseParams.status = statusMap[unifiedStatus]; baseParams.status = statusMap[unifiedStatus];
} }
} }
// 处理ID参数 // 处理ID参数
if (baseParams.id) { if (baseParams.id) {
baseParams.ids = baseParams.id; baseParams.ids = baseParams.id;
delete baseParams.id; delete baseParams.id;
} }
return baseParams; return baseParams;
} }
@ -724,7 +697,7 @@ export class ShopyyAdapter implements ISiteAdapter {
name: item.name || item.title, name: item.name || item.title,
type: String(item.product_type ?? ''), type: String(item.product_type ?? ''),
status: mapProductStatus(item.status), status: mapProductStatus(item.status),
sku: item.variant?.sku || item.variant?.sku_code || '', sku: item.variant?.sku || '',
regular_price: String(item.variant?.price ?? ''), regular_price: String(item.variant?.price ?? ''),
sale_price: String(item.special_price ?? ''), sale_price: String(item.special_price ?? ''),
price: String(item.price ?? ''), price: String(item.price ?? ''),
@ -753,7 +726,7 @@ export class ShopyyAdapter implements ISiteAdapter {
name: c.title || '', name: c.title || '',
})), })),
variations: item.variants?.map(this.mapPlatformToUnifiedVariation.bind(this)) || [], variations: item.variants?.map(this.mapPlatformToUnifiedVariation.bind(this)) || [],
permalink: `${this.site.websiteUrl}/products/${item.handle}`, permalink: `${this.site.websiteUrl}/products/${item.handle}`,
date_created: date_created:
typeof item.created_at === 'number' typeof item.created_at === 'number'
? new Date(item.created_at * 1000).toISOString() ? new Date(item.created_at * 1000).toISOString()
@ -824,7 +797,7 @@ export class ShopyyAdapter implements ISiteAdapter {
return status === 'publish' ? 1 : 0; return status === 'publish' ? 1 : 0;
}; };
// 构建 ShopYY 产品更新参数(仅包含传入的字段) // 构建 ShopYY 产品更新参数(仅包含传入的字段)
const params: any = {}; const params: any = {};
// 仅当字段存在时才添加到更新参数中 // 仅当字段存在时才添加到更新参数中
@ -843,7 +816,7 @@ export class ShopyyAdapter implements ISiteAdapter {
params.inventory_quantity = data.stock_status === 'instock' ? (data.stock_quantity || 1) : 0; params.inventory_quantity = data.stock_status === 'instock' ? (data.stock_quantity || 1) : 0;
} }
// 添加变体信息(如果存在) // 添加变体信息(如果存在)
if (data.variations && data.variations.length > 0) { if (data.variations && data.variations.length > 0) {
params.variants = data.variations.map((variation: UnifiedProductVariationDTO) => { params.variants = data.variations.map((variation: UnifiedProductVariationDTO) => {
const variationParams: any = {}; const variationParams: any = {};
@ -863,7 +836,7 @@ export class ShopyyAdapter implements ISiteAdapter {
}); });
} }
// 添加图片信息(如果存在) // 添加图片信息(如果存在)
if (data.images && data.images.length > 0) { if (data.images && data.images.length > 0) {
params.images = data.images.map((image: any) => ({ params.images = data.images.map((image: any) => ({
id: image.id, id: image.id,
@ -873,12 +846,12 @@ export class ShopyyAdapter implements ISiteAdapter {
})); }));
} }
// 添加标签信息(如果存在) // 添加标签信息(如果存在)
if (data.tags && data.tags.length > 0) { if (data.tags && data.tags.length > 0) {
params.tags = data.tags.map((tag: any) => tag.name || ''); params.tags = data.tags.map((tag: any) => tag.name || '');
} }
// 添加分类信息(如果存在) // 添加分类信息(如果存在)
if (data.categories && data.categories.length > 0) { if (data.categories && data.categories.length > 0) {
params.collections = data.categories.map((category: any) => ({ params.collections = data.categories.map((category: any) => ({
id: category.id, id: category.id,
@ -889,8 +862,8 @@ export class ShopyyAdapter implements ISiteAdapter {
return params; return params;
} }
async getProduct(where: { id?: string | number, sku?: string }): Promise<UnifiedProductDTO> { async getProduct(where: {id?: string | number, sku?: string}): Promise<UnifiedProductDTO> {
if (!where.id && !where.sku) { if(!where.id && !where.sku){
throw new Error('必须传入 id 或 sku') throw new Error('必须传入 id 或 sku')
} }
if (where.id) { if (where.id) {
@ -926,11 +899,11 @@ export class ShopyyAdapter implements ISiteAdapter {
per_page, per_page,
}; };
} }
mapAllProductParams(params: UnifiedSearchParamsDTO): Partial<ShopyyAllProductQuery> { mapAllProductParams(params: UnifiedSearchParamsDTO): Partial<ShopyyAllProductQuery>{
const mapped = { const mapped = {
...params.where, ...params.where,
} as any } as any
if (params.per_page) { mapped.limit = params.per_page } if(params.per_page){mapped.limit = params.per_page}
return mapped return mapped
} }
@ -944,7 +917,7 @@ export class ShopyyAdapter implements ISiteAdapter {
null, null,
requestParams requestParams
); );
if (response.code !== 0) { if(response.code !==0){
throw new Error(response.msg || '获取产品列表失败') throw new Error(response.msg || '获取产品列表失败')
} }
const { data = [] } = response; const { data = [] } = response;
@ -959,7 +932,7 @@ export class ShopyyAdapter implements ISiteAdapter {
return this.mapPlatformToUnifiedProduct(res); return this.mapPlatformToUnifiedProduct(res);
} }
async updateProduct(where: { id?: string | number, sku?: string }, data: Partial<UnifiedProductDTO>): Promise<boolean> { async updateProduct(where: {id?: string | number, sku?: string}, data: Partial<UnifiedProductDTO>): Promise<boolean> {
let productId: string; let productId: string;
if (where.id) { if (where.id) {
productId = String(where.id); productId = String(where.id);
@ -976,7 +949,7 @@ export class ShopyyAdapter implements ISiteAdapter {
return true; return true;
} }
async deleteProduct(where: { id?: string | number, sku?: string }): Promise<boolean> { async deleteProduct(where: {id?: string | number, sku?: string}): Promise<boolean> {
let productId: string | number; let productId: string | number;
if (where.id) { if (where.id) {
productId = where.id; productId = where.id;
@ -991,11 +964,11 @@ export class ShopyyAdapter implements ISiteAdapter {
await this.shopyyService.batchProcessProducts(this.site, { delete: [productId] }); await this.shopyyService.batchProcessProducts(this.site, { delete: [productId] });
return true; return true;
} }
// 通过sku获取产品详情的私有方法 // 通过sku获取产品详情的私有方法
private async getProductBySku(sku: string): Promise<UnifiedProductDTO> { private async getProductBySku(sku: string): Promise<UnifiedProductDTO> {
// 使用Shopyy API的搜索功能通过sku查询产品 // 使用Shopyy API的搜索功能通过sku查询产品
const response = await this.getAllProducts({ where: { sku } }); const response = await this.getAllProducts({ where: {sku} });
console.log('getProductBySku', response) console.log('getProductBySku', response)
const product = response?.[0] const product = response?.[0]
if (!product) { if (!product) {
@ -1059,12 +1032,12 @@ export class ShopyyAdapter implements ISiteAdapter {
return this.mapPlatformToUnifiedReview(createdReview); return this.mapPlatformToUnifiedReview(createdReview);
} }
async updateReview(where: { id: string | number }, data: any): Promise<UnifiedReviewDTO> { async updateReview(where: {id: string | number}, data: any): Promise<UnifiedReviewDTO> {
const updatedReview = await this.shopyyService.updateReview(this.site, where.id, data); const updatedReview = await this.shopyyService.updateReview(this.site, where.id, data);
return this.mapPlatformToUnifiedReview(updatedReview); return this.mapPlatformToUnifiedReview(updatedReview);
} }
async deleteReview(where: { id: string | number }): Promise<boolean> { async deleteReview(where: {id: string | number}): Promise<boolean> {
return await this.shopyyService.deleteReview(this.site, where.id); return await this.shopyyService.deleteReview(this.site, where.id);
} }
@ -1126,11 +1099,10 @@ export class ShopyyAdapter implements ISiteAdapter {
// ========== 产品变体映射方法 ========== // ========== 产品变体映射方法 ==========
mapPlatformToUnifiedVariation(variant: ShopyyVariant): UnifiedProductVariationDTO { mapPlatformToUnifiedVariation(variant: ShopyyVariant): UnifiedProductVariationDTO {
// 映射变体 // 映射变体
console.log('ivarianttem', variant)
return { return {
id: variant.id, id: variant.id,
name: variant.title || '', name: variant.sku || '',
sku: variant.sku || variant.sku_code || '', sku: variant.sku || '',
regular_price: String(variant.price ?? ''), regular_price: String(variant.price ?? ''),
sale_price: String(variant.special_price ?? ''), sale_price: String(variant.special_price ?? ''),
price: String(variant.price ?? ''), price: String(variant.price ?? ''),
@ -1149,7 +1121,7 @@ export class ShopyyAdapter implements ISiteAdapter {
} }
mapUpdateVariationParams(data: Partial<UnifiedProductVariationDTO>): any { mapUpdateVariationParams(data: Partial<UnifiedProductVariationDTO>): any {
// 构建 ShopYY 变体更新参数(仅包含传入的字段) // 构建 ShopYY 变体更新参数(仅包含传入的字段)
const params: any = {}; const params: any = {};
// 仅当字段存在时才添加到更新参数中 // 仅当字段存在时才添加到更新参数中
@ -1221,7 +1193,7 @@ export class ShopyyAdapter implements ISiteAdapter {
return data return data
} }
async getWebhook(where: { id: string | number }): Promise<UnifiedWebhookDTO> { async getWebhook(where: {id: string | number}): Promise<UnifiedWebhookDTO> {
const webhook = await this.shopyyService.getWebhook(this.site, where.id); const webhook = await this.shopyyService.getWebhook(this.site, where.id);
return this.mapPlatformToUnifiedWebhook(webhook); return this.mapPlatformToUnifiedWebhook(webhook);
} }
@ -1247,12 +1219,12 @@ export class ShopyyAdapter implements ISiteAdapter {
return this.mapPlatformToUnifiedWebhook(createdWebhook); return this.mapPlatformToUnifiedWebhook(createdWebhook);
} }
async updateWebhook(where: { id: string | number }, data: UpdateWebhookDTO): Promise<UnifiedWebhookDTO> { async updateWebhook(where: {id: string | number}, data: UpdateWebhookDTO): Promise<UnifiedWebhookDTO> {
const updatedWebhook = await this.shopyyService.updateWebhook(this.site, where.id, data); const updatedWebhook = await this.shopyyService.updateWebhook(this.site, where.id, data);
return this.mapPlatformToUnifiedWebhook(updatedWebhook); return this.mapPlatformToUnifiedWebhook(updatedWebhook);
} }
async deleteWebhook(where: { id: string | number }): Promise<boolean> { async deleteWebhook(where: {id: string | number}): Promise<boolean> {
return await this.shopyyService.deleteWebhook(this.site, where.id); return await this.shopyyService.deleteWebhook(this.site, where.id);
} }
@ -1295,14 +1267,14 @@ export class ShopyyAdapter implements ISiteAdapter {
// 处理分页参数 // 处理分页参数
const page = Number(params.page || 1); const page = Number(params.page || 1);
const limit = Number(params.per_page ?? 20); const limit = Number(params.per_page ?? 20);
// 处理 where 条件 // 处理 where 条件
const query: any = { const query: any = {
...(params.where || {}), ...(params.where || {}),
page, page,
limit, limit,
} }
if (params.orderBy) { if(params.orderBy){
const [field, dir] = Object.entries(params.orderBy)[0]; const [field, dir] = Object.entries(params.orderBy)[0];
query.order_by = dir === 'desc' ? 'desc' : 'asc'; query.order_by = dir === 'desc' ? 'desc' : 'asc';
query.order_field = field query.order_field = field
@ -1312,24 +1284,24 @@ export class ShopyyAdapter implements ISiteAdapter {
// 映射产品状态: publish -> 1, draft -> 0 // 映射产品状态: publish -> 1, draft -> 0
mapStatus = (status: string) => { mapStatus = (status: string) => {
return status === 'publish' ? 1 : 0; return status === 'publish' ? 1 : 0;
}; };
// 映射库存状态: instock -> 1, outofstock -> 0 // 映射库存状态: instock -> 1, outofstock -> 0
mapStockStatus = (stockStatus: string) => { mapStockStatus = (stockStatus: string) => {
return stockStatus === 'instock' ? 1 : 0; return stockStatus === 'instock' ? 1 : 0;
}; };
shopyyOrderStatusMap = {//订单状态 100 未完成110 待处理180 已完成(确认收货); 190 取消; shopyyOrderStatusMap = {//订单状态 100 未完成110 待处理180 已完成(确认收货); 190 取消;
[100]: OrderStatus.PENDING, // 100 未完成 转为 pending [100]: OrderStatus.PENDING, // 100 未完成 转为 pending
[110]: OrderStatus.PROCESSING, // 110 待处理 转为 processing [110]: OrderStatus.PROCESSING, // 110 待处理 转为 processing
// 已发货 // 已发货
[180]: OrderStatus.COMPLETED, // 180 已完成(确认收货) 转为 completed [180]: OrderStatus.COMPLETED, // 180 已完成(确认收货) 转为 completed
[190]: OrderStatus.CANCEL // 190 取消 转为 cancelled [190]: OrderStatus.CANCEL // 190 取消 转为 cancelled
} }
// 物流状态 300 未发货310 部分发货320 已发货330(确认收货)
fulfillmentStatusMap = { shopyyFulfillmentStatusMap = {
// 未发货 // 未发货
'300': OrderFulfillmentStatus.PENDING, '300': OrderFulfillmentStatus.PENDING,
// 部分发货 // 部分发货
@ -1340,23 +1312,4 @@ export class ShopyyAdapter implements ISiteAdapter {
'330': OrderFulfillmentStatus.CANCELLED, '330': OrderFulfillmentStatus.CANCELLED,
// 确认发货 // 确认发货
} }
// 支付状态 200 待支付210 支付中220 部分支付230 已支付240 支付失败250 部分退款260 已退款 290 已取消;
financialStatusMap = {
// 待支付
'200': OrderPaymentStatus.PENDING,
// 支付中
'210': OrderPaymentStatus.PAYING,
// 部分支付
'220': OrderPaymentStatus.PARTIALLY_PAID,
// 已支付
'230': OrderPaymentStatus.PAID,
// 支付失败
'240': OrderPaymentStatus.FAILED,
// 部分退款
'250': OrderPaymentStatus.PARTIALLY_REFUNDED,
// 已退款
'260': OrderPaymentStatus.REFUNDED,
// 已取消
'290': OrderPaymentStatus.CANCELLED,
}
} }

View File

@ -17,7 +17,6 @@ import {
UnifiedVariationPaginationDTO, UnifiedVariationPaginationDTO,
CreateReviewDTO, CreateReviewDTO,
UpdateReviewDTO, UpdateReviewDTO,
FulfillmentDTO,
} from '../dto/site-api.dto'; } from '../dto/site-api.dto';
import { UnifiedPaginationDTO, UnifiedSearchParamsDTO } from '../dto/api.dto'; import { UnifiedPaginationDTO, UnifiedSearchParamsDTO } from '../dto/api.dto';
import { import {
@ -29,13 +28,10 @@ import {
WooWebhook, WooWebhook,
WooOrderSearchParams, WooOrderSearchParams,
WooProductSearchParams, WooProductSearchParams,
WpMediaGetListParams,
WooFulfillment,
} from '../dto/woocommerce.dto'; } from '../dto/woocommerce.dto';
import { Site } from '../entity/site.entity'; import { Site } from '../entity/site.entity';
import { WPService } from '../service/wp.service'; import { WPService } from '../service/wp.service';
import { BatchOperationDTO, BatchOperationResultDTO } from '../dto/batch.dto'; import { BatchOperationDTO, BatchOperationResultDTO } from '../dto/batch.dto';
import { toArray, toNumber } from '../utils/trans.util';
export class WooCommerceAdapter implements ISiteAdapter { export class WooCommerceAdapter implements ISiteAdapter {
// 构造函数接收站点配置与服务实例 // 构造函数接收站点配置与服务实例
@ -253,25 +249,13 @@ export class WooCommerceAdapter implements ISiteAdapter {
date_modified: item.date_modified ?? item.modified, date_modified: item.date_modified ?? item.modified,
}; };
} }
mapMediaSearchParams(params: UnifiedSearchParamsDTO): Partial<WpMediaGetListParams> {
const page = params.page
const per_page = Number( params.per_page ?? 20);
return {
...params.where,
page,
per_page,
// orderby,
// order,
};
}
// 媒体操作方法 // 媒体操作方法
async getMedia(params: UnifiedSearchParamsDTO): Promise<UnifiedPaginationDTO<UnifiedMediaDTO>> { async getMedia(params: UnifiedSearchParamsDTO): Promise<UnifiedPaginationDTO<UnifiedMediaDTO>> {
// 获取媒体列表并映射为统一媒体DTO集合 // 获取媒体列表并映射为统一媒体DTO集合
const { items, total, totalPages, page, per_page } = await this.wpService.fetchMediaPaged( const { items, total, totalPages, page, per_page } = await this.wpService.fetchMediaPaged(
this.site, this.site,
this.mapMediaSearchParams(params) params
); );
return { return {
items: items.map(this.mapPlatformToUnifiedMedia.bind(this)), items: items.map(this.mapPlatformToUnifiedMedia.bind(this)),
@ -333,11 +317,22 @@ export class WooCommerceAdapter implements ISiteAdapter {
// } // }
const mapped: any = { const mapped: any = {
...(params.search ? { search: params.search } : {}), ...(params.search ? { search: params.search } : {}),
// ...(orderBy ? { orderBy } : {}),
page, page,
per_page, per_page,
}; };
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);
};
const toNumber = (value: any): number | undefined => {
if (value === undefined || value === null || value === '') return undefined;
const n = Number(value);
return Number.isFinite(n) ? n : undefined;
};
// 时间过滤参数 // 时间过滤参数
if (where.after ?? where.date_created_after ?? where.created_after) mapped.after = String(where.after ?? where.date_created_after ?? where.created_after); if (where.after ?? where.date_created_after ?? where.created_after) mapped.after = String(where.after ?? where.date_created_after ?? where.created_after);
@ -348,7 +343,8 @@ export class WooCommerceAdapter implements ISiteAdapter {
// 集合过滤参数 // 集合过滤参数
if (where.exclude) mapped.exclude = toArray(where.exclude); if (where.exclude) mapped.exclude = toArray(where.exclude);
if (where.ids || where.number || where.id || where.include) mapped.include = [...new Set([where.number,where.id,...toArray(where.ids),...toArray(where.include)])].filter(Boolean); if (where.include) mapped.include = toArray(where.include);
if (where.ids) mapped.include = toArray(where.ids);
if (toNumber(where.offset) !== undefined) mapped.offset = Number(where.offset); if (toNumber(where.offset) !== undefined) mapped.offset = Number(where.offset);
if (where.parent ?? where.parentId) mapped.parent = toArray(where.parent ?? where.parentId); if (where.parent ?? where.parentId) mapped.parent = toArray(where.parent ?? where.parentId);
if (where.parent_exclude ?? where.parentExclude) mapped.parent_exclude = toArray(where.parent_exclude ?? where.parentExclude); if (where.parent_exclude ?? where.parentExclude) mapped.parent_exclude = toArray(where.parent_exclude ?? where.parentExclude);
@ -399,11 +395,13 @@ export class WooCommerceAdapter implements ISiteAdapter {
// 包含账单地址与收货地址以及创建与更新时间 // 包含账单地址与收货地址以及创建与更新时间
// 映射物流追踪信息,将后端格式转换为前端期望的格式 // 映射物流追踪信息,将后端格式转换为前端期望的格式
const fulfillments = (item.fulfillments || []).map((track) => ({ const fulfillments = (item.fulfillments || []).map((track: any) => ({
tracking_id: track.tracking_id, tracking_number: track.tracking_number || '',
tracking_number: track.tracking_number, shipping_provider: track.shipping_provider || '',
shipping_provider: track.tracking_provider, shipping_method: track.shipping_method || '',
date_created: track.data_sipped, status: track.status || '',
date_created: track.date_created || '',
items: track.items || [],
})); }));
return { return {
@ -530,25 +528,54 @@ export class WooCommerceAdapter implements ISiteAdapter {
return await this.wpService.getFulfillments(this.site, String(orderId)); return await this.wpService.getFulfillments(this.site, String(orderId));
} }
async createOrderFulfillment(orderId: string | number, data: FulfillmentDTO): Promise<any> { async createOrderFulfillment(orderId: string | number, data: {
const shipmentData: Partial<WooFulfillment> = { tracking_number: string;
tracking_provider: data.shipping_provider, shipping_provider: string;
shipping_method?: string;
status?: string;
date_created?: string;
items?: Array<{
order_item_id: number;
quantity: number;
}>;
}): Promise<any> {
const shipmentData: any = {
shipping_provider: data.shipping_provider,
tracking_number: data.tracking_number, tracking_number: data.tracking_number,
data_sipped: data.date_created, };
// items: data.items,
if (data.shipping_method) {
shipmentData.shipping_method = data.shipping_method;
} }
if (data.status) {
shipmentData.status = data.status;
}
if (data.date_created) {
shipmentData.date_created = data.date_created;
}
if (data.items) {
shipmentData.items = data.items;
}
const response = await this.wpService.createFulfillment(this.site, String(orderId), shipmentData); const response = await this.wpService.createFulfillment(this.site, String(orderId), shipmentData);
return response.data; return response.data;
} }
async updateOrderFulfillment(orderId: string | number, fulfillmentId: string, data: FulfillmentDTO): Promise<any> { async updateOrderFulfillment(orderId: string | number, fulfillmentId: string, data: {
const shipmentData: Partial<WooFulfillment> = { tracking_number?: string;
tracking_provider: data.shipping_provider, shipping_provider?: string;
tracking_number: data.tracking_number, shipping_method?: string;
data_sipped: data.date_created, status?: string;
// items: data.items, date_created?: string;
} items?: Array<{
return await this.wpService.updateFulfillment(this.site, String(orderId), fulfillmentId, shipmentData); order_item_id: number;
quantity: number;
}>;
}): Promise<any> {
return await this.wpService.updateFulfillment(this.site, String(orderId), fulfillmentId, data);
} }
async deleteOrderFulfillment(orderId: string | number, fulfillmentId: string): Promise<boolean> { async deleteOrderFulfillment(orderId: string | number, fulfillmentId: string): Promise<boolean> {
@ -582,7 +609,7 @@ export class WooCommerceAdapter implements ISiteAdapter {
// await api.put(`orders/${orderId}`, { status: 'processing' }); // await api.put(`orders/${orderId}`, { status: 'processing' });
// // 添加取消履行的备注 // // 添加取消履行的备注
// const note = `订单履行已取消${data.reason ? `,原因:${data.reason}` : ''}`; // const note = `订单履行已取消${data.reason ? `,原因${data.reason}` : ''}`;
// await api.post(`orders/${orderId}/notes`, { note, customer_note: true }); // await api.post(`orders/${orderId}/notes`, { note, customer_note: true });
// return { // return {
@ -606,7 +633,7 @@ export class WooCommerceAdapter implements ISiteAdapter {
name: data.name, name: data.name,
type: data.type, type: data.type,
status: data.status, status: data.status,
sku: data.sku, sku: data.sku,
regular_price: data.regular_price, regular_price: data.regular_price,
sale_price: data.sale_price, sale_price: data.sale_price,
price: data.price, price: data.price,
@ -671,14 +698,14 @@ export class WooCommerceAdapter implements ISiteAdapter {
})); }));
} }
// 映射变体数据(注意:WooCommerce API 中变体通常通过单独的端点处理) // 映射变体数据注意WooCommerce API 中变体通常通过单独的端点处理)
// 这里只映射变体的基本信息,具体创建/更新变体需要额外处理 // 这里只映射变体的基本信息,具体创建/更新变体需要额外处理
if (data.variations && Array.isArray(data.variations)) { if (data.variations && Array.isArray(data.variations)) {
// 对于WooProduct类型variations字段只存储变体ID // 对于WooProduct类型variations字段只存储变体ID
mapped.variations = data.variations.map(variation => variation.id as number); mapped.variations = data.variations.map(variation => variation.id as number);
} }
// 映射下载数据(如果产品是可下载的) // 映射下载数据(如果产品是可下载的)
// if (data.downloads && Array.isArray(data.downloads)) { // if (data.downloads && Array.isArray(data.downloads)) {
// mapped.downloads = data.downloads.map(download => ({ // mapped.downloads = data.downloads.map(download => ({
// id: download.id as number, // id: download.id as number,

View File

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

View File

@ -99,7 +99,7 @@ export class MainConfiguration {
} }
/** /**
* () *
*/ */
private async initializeDatabase(): Promise<void> { private async initializeDatabase(): Promise<void> {
// 使用注入的数据库配置 // 使用注入的数据库配置
@ -118,7 +118,8 @@ export class MainConfiguration {
}); });
try { try {
this.logger.info(`正在检查数据库是否存在...`+ JSON.stringify(typeormConfig)); this.logger.info('正在检查数据库是否存在...');
// 初始化临时数据源 // 初始化临时数据源
await tempDataSource.initialize(); await tempDataSource.initialize();

View File

@ -30,7 +30,7 @@ export class DictController {
// 从上传的文件列表中获取第一个文件 // 从上传的文件列表中获取第一个文件
const file = files[0]; const file = files[0];
// 调用服务层方法处理XLSX文件 // 调用服务层方法处理XLSX文件
const result = await this.dictService.importDictsFromTable(file.data); const result = await this.dictService.importDictsFromXLSX(file.data);
// 返回导入结果 // 返回导入结果
return result; return result;
} }

View File

@ -79,31 +79,6 @@ 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 }) @ApiOkResponse({ type: ProductRes })
@Post('/') @Post('/')
async createProduct(@Body() productData: CreateProductDTO) { async createProduct(@Body() productData: CreateProductDTO) {
@ -142,7 +117,7 @@ export class ProductController {
const file = files?.[0]; const file = files?.[0];
if (!file) return errorResponse('未接收到上传文件'); if (!file) return errorResponse('未接收到上传文件');
const result = await this.productService.importProductsFromTable(file); const result = await this.productService.importProductsCSV(file);
return successResponse(result); return successResponse(result);
} catch (error) { } catch (error) {
return errorResponse(error?.message || error); return errorResponse(error?.message || error);
@ -775,31 +750,4 @@ export class ProductController {
return errorResponse(error?.message || error); 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);
}
}
} }

View File

@ -299,7 +299,7 @@ export class SiteApiController {
} }
} }
// 平台特性:产品导出(特殊CSV走平台服务) // 平台特性产品导出特殊CSV走平台服务
@Get('/:siteId/links') @Get('/:siteId/links')
async getLinks( async getLinks(
@Param('siteId') siteId: number @Param('siteId') siteId: number
@ -429,7 +429,7 @@ export class SiteApiController {
} }
} }
// 平台特性:产品导入(特殊CSV走平台服务) // 平台特性产品导入特殊CSV走平台服务
@Post('/:siteId/products/import-special') @Post('/:siteId/products/import-special')
@ApiOkResponse({ type: Object }) @ApiOkResponse({ type: Object })
async importProductsSpecial( async importProductsSpecial(
@ -443,7 +443,7 @@ export class SiteApiController {
const created: any[] = []; const created: any[] = [];
const failed: any[] = []; const failed: any[] = [];
if (site.type === 'woocommerce') { if (site.type === 'woocommerce') {
// 解析 CSV 为对象数组(若传入 items 则优先 items) // 解析 CSV 为对象数组(若传入 items 则优先 items
let payloads = items; let payloads = items;
if (!payloads.length && csvText) { if (!payloads.length && csvText) {
const lines = csvText.split(/\r?\n/).filter(Boolean); const lines = csvText.split(/\r?\n/).filter(Boolean);

View File

@ -79,7 +79,7 @@ export class StatisticsController {
@ApiOkResponse() @ApiOkResponse()
@Get('/orderSource') @Get('/orderSource')
async getOrderSource(@Query() params) { async getOrderSorce(@Query() params) {
try { try {
return successResponse(await this.statisticsService.getOrderSorce(params)); return successResponse(await this.statisticsService.getOrderSorce(params));
} catch (error) { } catch (error) {

View File

@ -9,12 +9,13 @@ import {
} from '@midwayjs/decorator'; } from '@midwayjs/decorator';
import { Context } from '@midwayjs/koa'; import { Context } from '@midwayjs/koa';
import * as crypto from 'crypto'; import * as crypto from 'crypto';
import { SiteService } from '../service/site.service'; import { SiteService } from '../service/site.service';
import { OrderService } from '../service/order.service'; import { OrderService } from '../service/order.service';
import { SiteApiService } from '../service/site-api.service';
import {
UnifiedOrderDTO,
} from '../dto/site-api.dto';
@Controller('/webhook') @Controller('/webhook')
export class WebhookController { export class WebhookController {
@ -30,11 +31,9 @@ export class WebhookController {
@Logger() @Logger()
logger: ILogger; logger: ILogger;
@Inject() @Inject()
private readonly siteService: SiteService; private readonly siteService: SiteService;
@Inject()
private readonly siteApiService: SiteApiService;
// 移除配置中的站点数组,来源统一改为数据库 // 移除配置中的站点数组,来源统一改为数据库
@ -50,7 +49,7 @@ export class WebhookController {
@Query('siteId') siteIdStr: string, @Query('siteId') siteIdStr: string,
@Headers() header: any @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 signature = header['x-wc-webhook-signature'];
const topic = header['x-wc-webhook-topic']; const topic = header['x-wc-webhook-topic'];
const source = header['x-wc-webhook-source']; const source = header['x-wc-webhook-source'];
@ -80,44 +79,43 @@ export class WebhookController {
.update(rawBody) .update(rawBody)
.digest('base64'); .digest('base64');
try { try {
if (hash !== signature) { if (hash === signature) {
switch (topic) {
case 'product.created':
case 'product.updated':
// 不再写入本地,平台事件仅确认接收
break;
case 'product.deleted':
// 不再写入本地,平台事件仅确认接收
break;
case 'order.created':
case 'order.updated':
await this.orderService.syncSingleOrder(siteId, body);
break;
case 'order.deleted':
break;
case 'customer.created':
break;
case 'customer.updated':
break;
case 'customer.deleted':
break;
default:
console.log('Unhandled event:', body.event);
}
return {
code: 200,
success: true,
message: 'Webhook processed successfully',
};
} else {
return { return {
code: 403, code: 403,
success: false, success: false,
message: 'Webhook verification failed', 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) { } catch (error) {
console.log(error); console.log(error);
} }
@ -132,10 +130,23 @@ export class WebhookController {
@Query('signature') signature: string, @Query('signature') signature: string,
@Headers() header: any @Headers() header: any
) { ) {
console.log(`webhook shoppy`, siteIdStr, body, header)
const topic = header['x-oemsaas-event-type']; 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 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) { if (!signature) {
return { return {
@ -151,7 +162,6 @@ export class WebhookController {
// .createHmac('sha256', this.secret) // .createHmac('sha256', this.secret)
// .update(rawBody) // .update(rawBody)
// .digest('base64'); // .digest('base64');
const adapter = await this.siteApiService.getAdapter(siteId);
try { try {
if (this.secret === signature) { if (this.secret === signature) {
switch (topic) { switch (topic) {
@ -164,8 +174,7 @@ export class WebhookController {
break; break;
case 'orders/create': case 'orders/create':
case 'orders/update': case 'orders/update':
const order = adapter.mapPlatformToUnifiedOrder(body) await this.orderService.syncSingleOrder(siteId, bodys);
await this.orderService.syncSingleOrder(siteId, order);
break; break;
case 'orders/delete': case 'orders/delete':
break; break;
@ -179,15 +188,20 @@ export class WebhookController {
console.log('Unhandled event:', topic); console.log('Unhandled event:', topic);
} }
return { return {
code: 200, code: 200,
success: true, success: true,
message: 'Webhook processed successfully', message: 'Webhook processed successfully',
}; };
} else {
return {
code: 403,
success: false,
message: 'Webhook verification failed',
};
} }
} catch (error) { } catch (error) {
console.log(error); console.log(error);
} }
} }
} }

View File

@ -126,7 +126,7 @@ const flavorsData = [
{ name: 'arctic-mint', title: 'arctic mint', titleCn: '北极薄荷', shortName: 'AR' }, { name: 'arctic-mint', title: 'arctic mint', titleCn: '北极薄荷', shortName: 'AR' },
{ name: 'baddie-blueberries', title: 'baddie blueberries', titleCn: '时髦蓝莓', shortName: 'BA' }, { name: 'baddie-blueberries', title: 'baddie blueberries', titleCn: '时髦蓝莓', shortName: 'BA' },
{ name: 'banana', title: 'banana', titleCn: '香蕉', shortName: 'BA' }, { name: 'banana', title: 'banana', titleCn: '香蕉', shortName: 'BA' },
{ name: 'banana-(solid)', title: 'banana (solid)', titleCn: '香蕉(固体)', shortName: 'BA' }, { name: 'banana-(solid)', title: 'banana (solid)', titleCn: '香蕉(固体)', shortName: 'BA' },
{ name: 'banana-berry', title: 'banana berry', titleCn: '香蕉莓果', shortName: 'BA' }, { name: 'banana-berry', title: 'banana berry', titleCn: '香蕉莓果', shortName: 'BA' },
{ name: 'banana-berry-melon-ice', title: 'banana berry melon ice', titleCn: '香蕉莓果瓜冰', shortName: 'BA' }, { name: 'banana-berry-melon-ice', title: 'banana berry melon ice', titleCn: '香蕉莓果瓜冰', shortName: 'BA' },
{ name: 'banana-blackberry', title: 'banana blackberry', titleCn: '香蕉黑莓', shortName: 'BA' }, { name: 'banana-blackberry', title: 'banana blackberry', titleCn: '香蕉黑莓', shortName: 'BA' },
@ -137,7 +137,7 @@ const flavorsData = [
{ name: 'bangin-blood-orange-iced', title: 'bangin blood orange iced', titleCn: '爆炸血橙冰', shortName: 'BA' }, { name: 'bangin-blood-orange-iced', title: 'bangin blood orange iced', titleCn: '爆炸血橙冰', shortName: 'BA' },
{ name: 'berries-in-the-6ix', title: 'berries in the 6ix', titleCn: '多伦多莓果', shortName: 'BE' }, { name: 'berries-in-the-6ix', title: 'berries in the 6ix', titleCn: '多伦多莓果', shortName: 'BE' },
{ name: 'berry-burst', title: 'berry burst', titleCn: '浆果爆发', shortName: 'BE' }, { name: 'berry-burst', title: 'berry burst', titleCn: '浆果爆发', shortName: 'BE' },
{ name: 'berry-burst-(thermal)', title: 'berry burst (thermal)', titleCn: '浆果爆发(热感)', shortName: 'BE' }, { name: 'berry-burst-(thermal)', title: 'berry burst (thermal)', titleCn: '浆果爆发(热感)', shortName: 'BE' },
{ name: 'berry-ice', title: 'berry ice', titleCn: '浆果冰', shortName: 'BE' }, { name: 'berry-ice', title: 'berry ice', titleCn: '浆果冰', shortName: 'BE' },
{ name: 'berry-lime-ice', title: 'berry lime ice', titleCn: '浆果青柠冰', shortName: 'BE' }, { name: 'berry-lime-ice', title: 'berry lime ice', titleCn: '浆果青柠冰', shortName: 'BE' },
{ name: 'berry-trio-ice', title: 'berry trio ice', titleCn: '三重浆果冰', shortName: 'BE' }, { name: 'berry-trio-ice', title: 'berry trio ice', titleCn: '三重浆果冰', shortName: 'BE' },
@ -145,7 +145,7 @@ const flavorsData = [
{ name: 'black-cherry', title: 'black cherry', titleCn: '黑樱桃', shortName: 'BL' }, { name: 'black-cherry', title: 'black cherry', titleCn: '黑樱桃', shortName: 'BL' },
{ name: 'blackcherry', title: 'blackcherry', titleCn: '黑樱桃混合', shortName: 'BL' }, { name: 'blackcherry', title: 'blackcherry', titleCn: '黑樱桃混合', shortName: 'BL' },
{ name: 'blackcurrant-ice', title: 'blackcurrant ice', titleCn: '黑加仑冰', shortName: 'BL' }, { name: 'blackcurrant-ice', title: 'blackcurrant ice', titleCn: '黑加仑冰', shortName: 'BL' },
{ name: 'black-currant-ice', title: 'black currant ice', titleCn: '黑加仑冰(空格版)', shortName: 'BL' }, { name: 'black-currant-ice', title: 'black currant ice', titleCn: '黑加仑冰(空格版)', shortName: 'BL' },
{ name: 'black-licorice', title: 'black licorice', titleCn: '黑甘草', shortName: 'BL' }, { name: 'black-licorice', title: 'black licorice', titleCn: '黑甘草', shortName: 'BL' },
{ name: 'black-tea', title: 'black tea', titleCn: '红茶', shortName: 'BL' }, { name: 'black-tea', title: 'black tea', titleCn: '红茶', shortName: 'BL' },
{ name: 'blackberry-ice', title: 'blackberry ice', titleCn: '黑莓冰', shortName: 'BL' }, { name: 'blackberry-ice', title: 'blackberry ice', titleCn: '黑莓冰', shortName: 'BL' },
@ -168,7 +168,7 @@ const flavorsData = [
{ name: 'blue-razz', title: 'blue razz', titleCn: '蓝覆盆子', shortName: 'BL' }, { name: 'blue-razz', title: 'blue razz', titleCn: '蓝覆盆子', shortName: 'BL' },
{ name: 'blue-razz-hype', title: 'blue razz hype', titleCn: '蓝覆盆子热情', shortName: 'BL' }, { name: 'blue-razz-hype', title: 'blue razz hype', titleCn: '蓝覆盆子热情', shortName: 'BL' },
{ name: 'blue-razz-ice', title: 'blue razz ice', titleCn: '蓝覆盆子冰', shortName: 'BL' }, { name: 'blue-razz-ice', title: 'blue razz ice', titleCn: '蓝覆盆子冰', shortName: 'BL' },
{ name: 'blue-razz-ice-(solid)', title: 'blue razz ice (solid)', titleCn: '蓝覆盆子冰(固体)', shortName: 'BL' }, { name: 'blue-razz-ice-(solid)', title: 'blue razz ice (solid)', titleCn: '蓝覆盆子冰(固体)', shortName: 'BL' },
{ name: 'blue-razz-ice-glace', title: 'blue razz ice glace', titleCn: '蓝覆盆子冰格', shortName: 'BL' }, { name: 'blue-razz-ice-glace', title: 'blue razz ice glace', titleCn: '蓝覆盆子冰格', shortName: 'BL' },
{ name: 'blue-razz-lemon-ice', title: 'blue razz lemon ice', titleCn: '蓝覆盆子柠檬冰', shortName: 'BL' }, { name: 'blue-razz-lemon-ice', title: 'blue razz lemon ice', titleCn: '蓝覆盆子柠檬冰', shortName: 'BL' },
{ name: 'blue-razz-lemonade', title: 'blue razz lemonade', titleCn: '蓝覆盆子柠檬水', shortName: 'BL' }, { name: 'blue-razz-lemonade', title: 'blue razz lemonade', titleCn: '蓝覆盆子柠檬水', shortName: 'BL' },
@ -196,7 +196,7 @@ const flavorsData = [
{ name: 'bumpin-blackcurrant-iced', title: 'bumpin blackcurrant iced', titleCn: '黑加仑热烈冰', shortName: 'BU' }, { name: 'bumpin-blackcurrant-iced', title: 'bumpin blackcurrant iced', titleCn: '黑加仑热烈冰', shortName: 'BU' },
{ name: 'burst-ice', title: 'burst ice', titleCn: '爆炸冰', shortName: 'BU' }, { name: 'burst-ice', title: 'burst ice', titleCn: '爆炸冰', shortName: 'BU' },
{ name: 'bussin-banana-iced', title: 'bussin banana iced', titleCn: '香蕉热烈冰', shortName: 'BU' }, { name: 'bussin-banana-iced', title: 'bussin banana iced', titleCn: '香蕉热烈冰', shortName: 'BU' },
{ name: 'bussin-banana-iced', title: 'bussin banana iced', titleCn: '香蕉热烈冰(重复)', shortName: 'BU' }, { name: 'bussin-banana-iced', title: 'bussin banana iced', titleCn: '香蕉热烈冰(重复)', shortName: 'BU' },
{ name: 'california-cherry', title: 'california cherry', titleCn: '加州樱桃', shortName: 'CA' }, { name: 'california-cherry', title: 'california cherry', titleCn: '加州樱桃', shortName: 'CA' },
{ name: 'cantaloupe-mango-banana', title: 'cantaloupe mango banana', titleCn: '香瓜芒果香蕉', shortName: 'CA' }, { name: 'cantaloupe-mango-banana', title: 'cantaloupe mango banana', titleCn: '香瓜芒果香蕉', shortName: 'CA' },
{ name: 'caramel', title: 'caramel', titleCn: '焦糖', shortName: 'CA' }, { name: 'caramel', title: 'caramel', titleCn: '焦糖', shortName: 'CA' },
@ -230,7 +230,7 @@ const flavorsData = [
{ name: 'citrus-chill', title: 'citrus chill', titleCn: '柑橘清凉', shortName: 'CI' }, { name: 'citrus-chill', title: 'citrus chill', titleCn: '柑橘清凉', shortName: 'CI' },
{ name: 'citrus-smash-ice', title: 'citrus smash ice', titleCn: '柑橘冲击冰', shortName: 'CI' }, { name: 'citrus-smash-ice', title: 'citrus smash ice', titleCn: '柑橘冲击冰', shortName: 'CI' },
{ name: 'citrus-sunrise', title: 'citrus sunrise', titleCn: '柑橘日出', shortName: 'CI' }, { name: 'citrus-sunrise', title: 'citrus sunrise', titleCn: '柑橘日出', shortName: 'CI' },
{ name: 'citrus-sunrise-(thermal)', title: 'citrus sunrise (thermal)', titleCn: '柑橘日出(热感)', shortName: 'CI' }, { name: 'citrus-sunrise-(thermal)', title: 'citrus sunrise (thermal)', titleCn: '柑橘日出(热感)', shortName: 'CI' },
{ name: 'classic', title: 'classic', titleCn: '经典', shortName: 'CL' }, { name: 'classic', title: 'classic', titleCn: '经典', shortName: 'CL' },
{ name: 'classic-ice', title: 'classic ice', titleCn: '经典冰', shortName: 'CL' }, { name: 'classic-ice', title: 'classic ice', titleCn: '经典冰', shortName: 'CL' },
{ name: 'classic-mint-ice', title: 'classic mint ice', titleCn: '经典薄荷冰', shortName: 'CL' }, { name: 'classic-mint-ice', title: 'classic mint ice', titleCn: '经典薄荷冰', shortName: 'CL' },
@ -310,7 +310,7 @@ const flavorsData = [
{ name: 'fizzy', title: 'fizzy', titleCn: '汽水', shortName: 'FI' }, { name: 'fizzy', title: 'fizzy', titleCn: '汽水', shortName: 'FI' },
{ name: 'flavourless', title: 'flavourless', titleCn: '无味', shortName: 'FL' }, { name: 'flavourless', title: 'flavourless', titleCn: '无味', shortName: 'FL' },
{ name: 'flippin-fruit-flash', title: 'flippin fruit flash', titleCn: '翻转水果闪电', shortName: 'FL' }, { name: 'flippin-fruit-flash', title: 'flippin fruit flash', titleCn: '翻转水果闪电', shortName: 'FL' },
{ name: 'flippin-fruit-flash-(rainbow-burst)', title: 'flippin fruit flash (rainbow burst)', titleCn: '翻转水果闪电(彩虹爆发)', shortName: 'FL' }, { name: 'flippin-fruit-flash-(rainbow-burst)', title: 'flippin fruit flash (rainbow burst)', titleCn: '翻转水果闪电(彩虹爆发)', shortName: 'FL' },
{ name: 'forest-fruits', title: 'forest fruits', titleCn: '森林水果', shortName: 'FO' }, { name: 'forest-fruits', title: 'forest fruits', titleCn: '森林水果', shortName: 'FO' },
{ name: 'fragrant-grapefruit', title: 'fragrant grapefruit', titleCn: '香气葡萄柚', shortName: 'FR' }, { name: 'fragrant-grapefruit', title: 'fragrant grapefruit', titleCn: '香气葡萄柚', shortName: 'FR' },
{ name: 'freeze', title: 'freeze', titleCn: '冰冻', shortName: 'FR' }, { name: 'freeze', title: 'freeze', titleCn: '冰冻', shortName: 'FR' },
@ -340,14 +340,14 @@ const flavorsData = [
{ name: 'fuji-melon-ice', title: 'fuji melon ice', titleCn: '富士瓜冰', shortName: 'FU' }, { name: 'fuji-melon-ice', title: 'fuji melon ice', titleCn: '富士瓜冰', shortName: 'FU' },
{ name: 'full-charge', title: 'full charge', titleCn: '满电', shortName: 'FU' }, { name: 'full-charge', title: 'full charge', titleCn: '满电', shortName: 'FU' },
{ name: 'gb', title: 'gb', titleCn: '软糖', shortName: 'GB' }, { name: 'gb', title: 'gb', titleCn: '软糖', shortName: 'GB' },
{ name: 'gb(gummy-bear)', title: 'gb(gummy bear)', titleCn: '软糖(Gummy Bear)', shortName: 'GB' }, { name: 'gb(gummy-bear)', title: 'gb(gummy bear)', titleCn: '软糖Gummy Bear', shortName: 'GB' },
{ name: 'gentle-mint', title: 'gentle mint', titleCn: '温和薄荷', shortName: 'GE' }, { name: 'gentle-mint', title: 'gentle mint', titleCn: '温和薄荷', shortName: 'GE' },
{ name: 'ghost-cola-&-vanilla', title: 'ghost cola & vanilla', titleCn: '幽灵可乐香草', shortName: 'GH' }, { name: 'ghost-cola-&-vanilla', title: 'ghost cola & vanilla', titleCn: '幽灵可乐香草', shortName: 'GH' },
{ name: 'ghost-cola-ice', title: 'ghost cola ice', titleCn: '幽灵可乐冰', shortName: 'GH' }, { name: 'ghost-cola-ice', title: 'ghost cola ice', titleCn: '幽灵可乐冰', shortName: 'GH' },
{ name: 'ghost-mango', title: 'ghost mango', titleCn: '幽灵芒果', shortName: 'GH' }, { name: 'ghost-mango', title: 'ghost mango', titleCn: '幽灵芒果', shortName: 'GH' },
{ name: 'ghost-original', title: 'ghost original', titleCn: '幽灵原味', shortName: 'GH' }, { name: 'ghost-original', title: 'ghost original', titleCn: '幽灵原味', shortName: 'GH' },
{ name: 'ghost-watermelon-ice', title: 'ghost watermelon ice', titleCn: '幽灵西瓜冰', shortName: 'GH' }, { name: 'ghost-watermelon-ice', title: 'ghost watermelon ice', titleCn: '幽灵西瓜冰', shortName: 'GH' },
{ name: 'gnarly-green-d-(green-dew)', title: 'gnarly green d (green dew)', titleCn: '狂野绿 D(绿色露水)', shortName: 'GN' }, { name: 'gnarly-green-d-(green-dew)', title: 'gnarly green d (green dew)', titleCn: '狂野绿 D(绿色露水)', shortName: 'GN' },
{ name: 'gold-edition', title: 'gold edition', titleCn: '金版', shortName: 'GO' }, { name: 'gold-edition', title: 'gold edition', titleCn: '金版', shortName: 'GO' },
{ name: 'grape', title: 'grape', titleCn: '葡萄', shortName: 'GR' }, { name: 'grape', title: 'grape', titleCn: '葡萄', shortName: 'GR' },
{ name: 'grape-cherry', title: 'grape cherry', titleCn: '葡萄樱桃', shortName: 'GR' }, { name: 'grape-cherry', title: 'grape cherry', titleCn: '葡萄樱桃', shortName: 'GR' },
@ -492,13 +492,13 @@ const flavorsData = [
{ name: 'mixed-fruit', title: 'mixed fruit', titleCn: '混合水果', shortName: 'MI' }, { name: 'mixed-fruit', title: 'mixed fruit', titleCn: '混合水果', shortName: 'MI' },
{ name: 'mocha-ice', title: 'mocha ice', titleCn: '摩卡冰', shortName: 'MO' }, { name: 'mocha-ice', title: 'mocha ice', titleCn: '摩卡冰', shortName: 'MO' },
{ name: 'morocco-mint', title: 'morocco mint', titleCn: '摩洛哥薄荷', shortName: 'MO' }, { name: 'morocco-mint', title: 'morocco mint', titleCn: '摩洛哥薄荷', shortName: 'MO' },
{ name: 'morocco-mint-(thermal)', title: 'morocco mint (thermal)', titleCn: '摩洛哥薄荷(热感)', shortName: 'MO' }, { name: 'morocco-mint-(thermal)', title: 'morocco mint (thermal)', titleCn: '摩洛哥薄荷(热感)', shortName: 'MO' },
{ name: 'mung-beans', title: 'mung beans', titleCn: '绿豆', shortName: 'MU' }, { name: 'mung-beans', title: 'mung beans', titleCn: '绿豆', shortName: 'MU' },
{ name: 'nasty-tropic', title: 'nasty tropic', titleCn: '恶搞热带', shortName: 'NA' }, { name: 'nasty-tropic', title: 'nasty tropic', titleCn: '恶搞热带', shortName: 'NA' },
{ name: 'nectarine-ice', title: 'nectarine ice', titleCn: '油桃冰', shortName: 'NE' }, { name: 'nectarine-ice', title: 'nectarine ice', titleCn: '油桃冰', shortName: 'NE' },
{ name: 'night-rider', title: 'night rider', titleCn: '夜骑', shortName: 'NI' }, { name: 'night-rider', title: 'night rider', titleCn: '夜骑', shortName: 'NI' },
{ name: 'nirvana', title: 'nirvana', titleCn: '宁静蓝莓', shortName: 'NI' }, { name: 'nirvana', title: 'nirvana', titleCn: '宁静蓝莓', shortName: 'NI' },
{ name: 'north-american-style(root-beer)', title: 'north american style(root beer)', titleCn: '北美风格(根啤)', shortName: 'NO' }, { name: 'north-american-style(root-beer)', title: 'north american style(root beer)', titleCn: '北美风格(根啤)', shortName: 'NO' },
{ name: 'northern-blue-razz', title: 'northern blue razz', titleCn: '北方蓝覆盆子', shortName: 'NO' }, { name: 'northern-blue-razz', title: 'northern blue razz', titleCn: '北方蓝覆盆子', shortName: 'NO' },
{ name: 'nutty-virginia', title: 'nutty virginia', titleCn: '坚果弗吉尼亚', shortName: 'NU' }, { name: 'nutty-virginia', title: 'nutty virginia', titleCn: '坚果弗吉尼亚', shortName: 'NU' },
{ name: 'orange', title: 'orange', titleCn: '橙子', shortName: 'OR' }, { name: 'orange', title: 'orange', titleCn: '橙子', shortName: 'OR' },
@ -508,12 +508,12 @@ const flavorsData = [
{ name: 'orange-mango-guava', title: 'orange mango guava', titleCn: '橙子芒果番石榴', shortName: 'OR' }, { name: 'orange-mango-guava', title: 'orange mango guava', titleCn: '橙子芒果番石榴', shortName: 'OR' },
{ name: 'orange-mango-pineapple-ice', title: 'orange mango pineapple ice', titleCn: '橙子芒果菠萝冰', shortName: 'OR' }, { name: 'orange-mango-pineapple-ice', title: 'orange mango pineapple ice', titleCn: '橙子芒果菠萝冰', shortName: 'OR' },
{ name: 'orange-p', title: 'orange p', titleCn: '橙子 P', shortName: 'OR' }, { name: 'orange-p', title: 'orange p', titleCn: '橙子 P', shortName: 'OR' },
{ name: 'orange-p(fanta)', title: 'orange p(fanta)', titleCn: '橙子 P(芬达)', shortName: 'OR' }, { name: 'orange-p(fanta)', title: 'orange p(fanta)', titleCn: '橙子 P(芬达)', shortName: 'OR' },
{ name: 'orange-spark', title: 'orange spark', titleCn: '橙色火花', shortName: 'OR' }, { name: 'orange-spark', title: 'orange spark', titleCn: '橙色火花', shortName: 'OR' },
{ name: 'orange-tangerine', title: 'orange tangerine', titleCn: '橙子柑橘', shortName: 'OR' }, { name: 'orange-tangerine', title: 'orange tangerine', titleCn: '橙子柑橘', shortName: 'OR' },
{ name: 'original', title: 'original', titleCn: '原味', shortName: 'OR' }, { name: 'original', title: 'original', titleCn: '原味', shortName: 'OR' },
{ name: 'packin-peach-berry', title: 'packin peach berry', titleCn: '装满桃浆果', shortName: 'PA' }, { name: 'packin-peach-berry', title: 'packin peach berry', titleCn: '装满桃浆果', shortName: 'PA' },
{ name: 'packin-peach-berry-(popn-peach-berry)', title: 'packin peach berry (popn peach berry)', titleCn: '装满桃浆果(Popn 桃浆果)', shortName: 'PA' }, { name: 'packin-peach-berry-(popn-peach-berry)', title: 'packin peach berry (popn peach berry)', titleCn: '装满桃浆果Popn 桃浆果)', shortName: 'PA' },
{ name: 'papio', title: 'papio', titleCn: 'Papio', shortName: 'PA' }, { name: 'papio', title: 'papio', titleCn: 'Papio', shortName: 'PA' },
{ name: 'paradise', title: 'paradise', titleCn: '天堂', shortName: 'PA' }, { name: 'paradise', title: 'paradise', titleCn: '天堂', shortName: 'PA' },
{ name: 'paradise-iced', title: 'paradise iced', titleCn: '天堂冰', shortName: 'PA' }, { name: 'paradise-iced', title: 'paradise iced', titleCn: '天堂冰', shortName: 'PA' },
@ -603,7 +603,7 @@ const flavorsData = [
{ name: 'red-fruits', title: 'red fruits', titleCn: '红色水果', shortName: 'RE' }, { name: 'red-fruits', title: 'red fruits', titleCn: '红色水果', shortName: 'RE' },
{ name: 'red-lightning', title: 'red lightning', titleCn: '红色闪电', shortName: 'RE' }, { name: 'red-lightning', title: 'red lightning', titleCn: '红色闪电', shortName: 'RE' },
{ name: 'red-line', title: 'red line', titleCn: '红线', shortName: 'RE' }, { name: 'red-line', title: 'red line', titleCn: '红线', shortName: 'RE' },
{ name: 'red-line-(energy-drink)', title: 'red line (energy drink)', titleCn: '红线(能量饮料)', shortName: 'RE' }, { name: 'red-line-(energy-drink)', title: 'red line (energy drink)', titleCn: '红线(能量饮料)', shortName: 'RE' },
{ name: 'red-magic', title: 'red magic', titleCn: '红魔', shortName: 'RE' }, { name: 'red-magic', title: 'red magic', titleCn: '红魔', shortName: 'RE' },
{ name: 'rich-tobacco', title: 'rich tobacco', titleCn: '浓烈烟草', shortName: 'RI' }, { name: 'rich-tobacco', title: 'rich tobacco', titleCn: '浓烈烟草', shortName: 'RI' },
{ name: 'root-beer', title: 'root beer', titleCn: '根啤', shortName: 'RO' }, { name: 'root-beer', title: 'root beer', titleCn: '根啤', shortName: 'RO' },
@ -625,8 +625,8 @@ const flavorsData = [
{ name: 'sic-strawberry-iced', title: 'sic strawberry iced', titleCn: '意大利草莓冰', shortName: 'SI' }, { name: 'sic-strawberry-iced', title: 'sic strawberry iced', titleCn: '意大利草莓冰', shortName: 'SI' },
{ name: 'simply-spearmint', title: 'simply spearmint', titleCn: '清爽留兰香', shortName: 'SI' }, { name: 'simply-spearmint', title: 'simply spearmint', titleCn: '清爽留兰香', shortName: 'SI' },
{ name: 'skc', title: 'skc', titleCn: 'SKC', shortName: 'SK' }, { name: 'skc', title: 'skc', titleCn: 'SKC', shortName: 'SK' },
{ name: 'skc(skittles-candy)', title: 'skc(skittles candy)', titleCn: 'SKC(彩虹糖)', shortName: 'SK' }, { name: 'skc(skittles-candy)', title: 'skc(skittles candy)', titleCn: 'SKC(彩虹糖)', shortName: 'SK' },
{ name: 'slammin-sts-(sour-snap)', title: 'slammin sts (sour snap)', titleCn: '热烈 STS(酸糖)', shortName: 'SL' }, { name: 'slammin-sts-(sour-snap)', title: 'slammin sts (sour snap)', titleCn: '热烈 STS(酸糖)', shortName: 'SL' },
{ name: 'slammin-sts-iced', title: 'slammin sts iced', titleCn: '热烈 STS 冰', shortName: 'SL' }, { name: 'slammin-sts-iced', title: 'slammin sts iced', titleCn: '热烈 STS 冰', shortName: 'SL' },
{ name: 'smooth', title: 'smooth', titleCn: '顺滑', shortName: 'SM' }, { name: 'smooth', title: 'smooth', titleCn: '顺滑', shortName: 'SM' },
{ name: 'smooth-mint', title: 'smooth mint', titleCn: '顺滑薄荷', shortName: 'SM' }, { name: 'smooth-mint', title: 'smooth mint', titleCn: '顺滑薄荷', shortName: 'SM' },
@ -664,7 +664,7 @@ const flavorsData = [
{ name: 'strawberry-jasmine-t', title: 'strawberry jasmine t', titleCn: '草莓茉莉茶', shortName: 'ST' }, { name: 'strawberry-jasmine-t', title: 'strawberry jasmine t', titleCn: '草莓茉莉茶', shortName: 'ST' },
{ name: 'strawberry-jasmine-tea', title: 'strawberry jasmine tea', titleCn: '草莓茉莉茶', shortName: 'ST' }, { name: 'strawberry-jasmine-tea', title: 'strawberry jasmine tea', titleCn: '草莓茉莉茶', shortName: 'ST' },
{ name: 'strawberry-kiwi', title: 'strawberry kiwi', titleCn: '草莓奇异果', shortName: 'ST' }, { name: 'strawberry-kiwi', title: 'strawberry kiwi', titleCn: '草莓奇异果', shortName: 'ST' },
{ name: 'strawberry-kiwi-(solid)', title: 'strawberry kiwi (solid)', titleCn: '草莓奇异果(固体)', shortName: 'ST' }, { name: 'strawberry-kiwi-(solid)', title: 'strawberry kiwi (solid)', titleCn: '草莓奇异果(固体)', shortName: 'ST' },
{ name: 'strawberry-kiwi-banana-ice', title: 'strawberry kiwi banana ice', titleCn: '草莓奇异果香蕉冰', shortName: 'ST' }, { name: 'strawberry-kiwi-banana-ice', title: 'strawberry kiwi banana ice', titleCn: '草莓奇异果香蕉冰', shortName: 'ST' },
{ name: 'strawberry-kiwi-guava-ice', title: 'strawberry kiwi guava ice', titleCn: '草莓奇异果番石榴冰', shortName: 'ST' }, { name: 'strawberry-kiwi-guava-ice', title: 'strawberry kiwi guava ice', titleCn: '草莓奇异果番石榴冰', shortName: 'ST' },
{ name: 'strawberry-kiwi-ice', title: 'strawberry kiwi ice', titleCn: '草莓奇异果冰', shortName: 'ST' }, { name: 'strawberry-kiwi-ice', title: 'strawberry kiwi ice', titleCn: '草莓奇异果冰', shortName: 'ST' },
@ -680,10 +680,10 @@ const flavorsData = [
{ name: 'strawberry-watermelon', title: 'strawberry watermelon', titleCn: '草莓西瓜', shortName: 'ST' }, { name: 'strawberry-watermelon', title: 'strawberry watermelon', titleCn: '草莓西瓜', shortName: 'ST' },
{ name: 'strawberry-watermelon-ice', title: 'strawberry watermelon ice', titleCn: '草莓西瓜冰', shortName: 'ST' }, { name: 'strawberry-watermelon-ice', title: 'strawberry watermelon ice', titleCn: '草莓西瓜冰', shortName: 'ST' },
{ name: 'strawmelon-peach', title: 'strawmelon peach', titleCn: '草莓桃', shortName: 'ST' }, { name: 'strawmelon-peach', title: 'strawmelon peach', titleCn: '草莓桃', shortName: 'ST' },
{ name: 'strawmelon-peach-(solid)', title: 'strawmelon peach (solid)', titleCn: '草莓桃(固体)', shortName: 'ST' }, { name: 'strawmelon-peach-(solid)', title: 'strawmelon peach (solid)', titleCn: '草莓桃(固体)', shortName: 'ST' },
{ name: 'strawnana-orange', title: 'strawnana orange', titleCn: '草莓香蕉橙', shortName: 'ST' }, { name: 'strawnana-orange', title: 'strawnana orange', titleCn: '草莓香蕉橙', shortName: 'ST' },
{ name: 'summer-grape', title: 'summer grape', titleCn: '夏日葡萄', shortName: 'SU' }, { name: 'summer-grape', title: 'summer grape', titleCn: '夏日葡萄', shortName: 'SU' },
{ name: 'summer-grape-(thermal)', title: 'summer grape (thermal)', titleCn: '夏日葡萄(热感)', shortName: 'SU' }, { name: 'summer-grape-(thermal)', title: 'summer grape (thermal)', titleCn: '夏日葡萄(热感)', shortName: 'SU' },
{ name: 'super-sour-blueberry-iced', title: 'super sour blueberry iced', titleCn: '超级酸蓝莓冰', shortName: 'SU' }, { name: 'super-sour-blueberry-iced', title: 'super sour blueberry iced', titleCn: '超级酸蓝莓冰', shortName: 'SU' },
{ name: 'super-spearmint', title: 'super spearmint', titleCn: '超级留兰香', shortName: 'SU' }, { name: 'super-spearmint', title: 'super spearmint', titleCn: '超级留兰香', shortName: 'SU' },
{ name: 'super-spearmint-iced', title: 'super spearmint iced', titleCn: '超级留兰香冰', shortName: 'SU' }, { name: 'super-spearmint-iced', title: 'super spearmint iced', titleCn: '超级留兰香冰', shortName: 'SU' },
@ -704,7 +704,7 @@ const flavorsData = [
{ name: 'tropical-orang-ice', title: 'tropical orang ice', titleCn: '热带橙冰', shortName: 'TR' }, { name: 'tropical-orang-ice', title: 'tropical orang ice', titleCn: '热带橙冰', shortName: 'TR' },
{ name: 'tropical-prism-blast', title: 'tropical prism blast', titleCn: '热带棱镜爆炸', shortName: 'TR' }, { name: 'tropical-prism-blast', title: 'tropical prism blast', titleCn: '热带棱镜爆炸', shortName: 'TR' },
{ name: 'tropical-splash', title: 'tropical splash', titleCn: '热带飞溅', shortName: 'TR' }, { name: 'tropical-splash', title: 'tropical splash', titleCn: '热带飞溅', shortName: 'TR' },
{ name: 'tropical-splash-(solid)', title: 'tropical splash (solid)', titleCn: '热带飞溅(固体)', shortName: 'TR' }, { name: 'tropical-splash-(solid)', title: 'tropical splash (solid)', titleCn: '热带飞溅(固体)', shortName: 'TR' },
{ name: 'tropical-storm-ice', title: 'tropical storm ice', titleCn: '热带风暴冰', shortName: 'TR' }, { name: 'tropical-storm-ice', title: 'tropical storm ice', titleCn: '热带风暴冰', shortName: 'TR' },
{ name: 'tropical-summer', title: 'tropical summer', titleCn: '热带夏日', shortName: 'TR' }, { name: 'tropical-summer', title: 'tropical summer', titleCn: '热带夏日', shortName: 'TR' },
{ name: 'tropika', title: 'tropika', titleCn: '热带果', shortName: 'TR' }, { name: 'tropika', title: 'tropika', titleCn: '热带果', shortName: 'TR' },
@ -728,7 +728,7 @@ const flavorsData = [
{ name: 'watermelon-cantaloupe-honeydew-ice', title: 'watermelon cantaloupe honeydew ice', titleCn: '西瓜香瓜蜜瓜冰', shortName: 'WA' }, { name: 'watermelon-cantaloupe-honeydew-ice', title: 'watermelon cantaloupe honeydew ice', titleCn: '西瓜香瓜蜜瓜冰', shortName: 'WA' },
{ name: 'watermelon-g', title: 'watermelon g', titleCn: '西瓜 G', shortName: 'WA' }, { name: 'watermelon-g', title: 'watermelon g', titleCn: '西瓜 G', shortName: 'WA' },
{ name: 'watermelon-ice', title: 'watermelon ice', titleCn: '西瓜冰', shortName: 'WA' }, { name: 'watermelon-ice', title: 'watermelon ice', titleCn: '西瓜冰', shortName: 'WA' },
{ name: 'watermelon-ice-(solid)', title: 'watermelon ice (solid)', titleCn: '西瓜冰(固体)', shortName: 'WA' }, { name: 'watermelon-ice-(solid)', title: 'watermelon ice (solid)', titleCn: '西瓜冰(固体)', shortName: 'WA' },
{ name: 'watermelon-lime-ice', title: 'watermelon lime ice', titleCn: '西瓜青柠冰', shortName: 'WA' }, { name: 'watermelon-lime-ice', title: 'watermelon lime ice', titleCn: '西瓜青柠冰', shortName: 'WA' },
{ name: 'watermelon-mango-tango', title: 'watermelon mango tango', titleCn: '西瓜芒果探戈', shortName: 'WA' }, { name: 'watermelon-mango-tango', title: 'watermelon mango tango', titleCn: '西瓜芒果探戈', shortName: 'WA' },
{ name: 'watermelona-cg', title: 'watermelona cg', titleCn: '西瓜 CG', shortName: 'WA' }, { name: 'watermelona-cg', title: 'watermelona cg', titleCn: '西瓜 CG', shortName: 'WA' },
@ -750,7 +750,7 @@ const flavorsData = [
{ name: 'wild-strawberry-watermelon', title: 'wild strawberry watermelon', titleCn: '野生草莓西瓜', shortName: 'WI' }, { name: 'wild-strawberry-watermelon', title: 'wild strawberry watermelon', titleCn: '野生草莓西瓜', shortName: 'WI' },
{ name: 'wild-white-grape', title: 'wild white grape', titleCn: '野生白葡萄', shortName: 'WI' }, { name: 'wild-white-grape', title: 'wild white grape', titleCn: '野生白葡萄', shortName: 'WI' },
{ name: 'wild-white-grape-ice', title: 'wild white grape ice', titleCn: '野生白葡萄冰', shortName: 'WI' }, { name: 'wild-white-grape-ice', title: 'wild white grape ice', titleCn: '野生白葡萄冰', shortName: 'WI' },
{ name: 'wild-white-grape-iced', title: 'wild white grape iced', titleCn: '野生白葡萄冰(冷饮)', shortName: 'WI' }, { name: 'wild-white-grape-iced', title: 'wild white grape iced', titleCn: '野生白葡萄冰(冷饮)', shortName: 'WI' },
{ name: 'winter-berry-ice', title: 'winter berry ice', titleCn: '冬季浆果冰', shortName: 'WI' }, { name: 'winter-berry-ice', title: 'winter berry ice', titleCn: '冬季浆果冰', shortName: 'WI' },
{ name: 'winter-green', title: 'winter green', titleCn: '冬青', shortName: 'WI' }, { name: 'winter-green', title: 'winter green', titleCn: '冬青', shortName: 'WI' },
{ name: 'wintergreen', title: 'wintergreen', titleCn: '冬青薄荷', shortName: 'WI' }, { name: 'wintergreen', title: 'wintergreen', titleCn: '冬青薄荷', shortName: 'WI' },

View File

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

View File

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

View File

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

View File

@ -19,22 +19,15 @@ export class ShipmentBookDTO {
@ApiProperty({ type: 'number', isArray: true }) @ApiProperty({ type: 'number', isArray: true })
@Rule(RuleType.array<number>().default([])) @Rule(RuleType.array<number>().default([]))
orderIds?: number[]; orderIds?: number[];
@ApiProperty()
@Rule(RuleType.string())
shipmentPlatform: string;
} }
export class ShipmentFeeBookDTO { export class ShipmentFeeBookDTO {
@ApiProperty()
shipmentPlatform: string;
@ApiProperty() @ApiProperty()
stockPointId: number; stockPointId: number;
@ApiProperty() @ApiProperty()
sender: string; sender: string;
@ApiProperty() @ApiProperty()
startPhone: string|any; startPhone: string;
@ApiProperty() @ApiProperty()
startPostalCode: string; startPostalCode: string;
@ApiProperty() @ApiProperty()
@ -70,8 +63,6 @@ export class ShipmentFeeBookDTO {
weight: number; weight: number;
@ApiProperty() @ApiProperty()
weightUom: string; weightUom: string;
@ApiProperty()
address_id: number;
} }
export class PaymentMethodDTO { export class PaymentMethodDTO {

View File

@ -98,9 +98,13 @@ export class QueryOrderDTO {
} }
export class QueryOrderSalesDTO { export class QueryOrderSalesDTO {
@ApiProperty({ description: '是否为原产品还是库存产品' }) @ApiProperty()
@Rule(RuleType.bool().default(false)) @Rule(RuleType.bool().default(false))
isSource: boolean; isSource: boolean;
@ApiProperty()
@Rule(RuleType.bool().default(false))
exceptPackage: boolean;
@ApiProperty({ example: '1', description: '页码' }) @ApiProperty({ example: '1', description: '页码' })
@Rule(RuleType.number()) @Rule(RuleType.number())
@ -110,31 +114,19 @@ export class QueryOrderSalesDTO {
@Rule(RuleType.number()) @Rule(RuleType.number())
pageSize: number; pageSize: number;
@ApiProperty({ description: '排序对象,格式如 { productName: "asc", sku: "desc" }',type: 'any', required: false }) @ApiProperty()
@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()) @Rule(RuleType.number())
siteId: number; siteId: number;
@ApiProperty({ description: '名称' }) @ApiProperty()
@Rule(RuleType.string()) @Rule(RuleType.string())
name: string; name: string;
@ApiProperty({ description: 'SKU' }) @ApiProperty()
@Rule(RuleType.string())
sku: string;
@ApiProperty({ description: '开始日期' })
@Rule(RuleType.date()) @Rule(RuleType.date())
startDate: Date; startDate: Date;
@ApiProperty({ description: '结束日期' }) @ApiProperty()
@Rule(RuleType.date()) @Rule(RuleType.date())
endDate: Date; endDate: Date;
} }

View File

@ -59,10 +59,6 @@ export class CreateProductDTO {
@Rule(RuleType.number()) @Rule(RuleType.number())
categoryId?: number; categoryId?: number;
@ApiProperty({ description: '分类名称', required: false })
@Rule(RuleType.string().optional())
categoryName?: string;
@ApiProperty({ description: '站点 SKU 列表', type: 'array', required: false }) @ApiProperty({ description: '站点 SKU 列表', type: 'array', required: false })
@Rule(RuleType.array().items(RuleType.string()).optional()) @Rule(RuleType.array().items(RuleType.string()).optional())
siteSkus?: string[]; siteSkus?: string[];
@ -90,10 +86,7 @@ export class CreateProductDTO {
@Rule(RuleType.number()) @Rule(RuleType.number())
promotionPrice?: number; promotionPrice?: number;
// 产品图片URL
@ApiProperty({ description: '产品图片URL', example: 'https://example.com/image.jpg', required: false })
@Rule(RuleType.string().optional())
image?: string;
// 商品类型(默认 single; bundle 需手动设置组成) // 商品类型(默认 single; bundle 需手动设置组成)
@ApiProperty({ description: '商品类型', enum: ['single', 'bundle'], default: 'single', required: false }) @ApiProperty({ description: '商品类型', enum: ['single', 'bundle'], default: 'single', required: false })
@ -146,10 +139,6 @@ export class UpdateProductDTO {
@Rule(RuleType.number()) @Rule(RuleType.number())
categoryId?: number; categoryId?: number;
@ApiProperty({ description: '分类名称', required: false })
@Rule(RuleType.string().optional())
categoryName?: string;
@ApiProperty({ description: '站点 SKU 列表', type: 'array', required: false }) @ApiProperty({ description: '站点 SKU 列表', type: 'array', required: false })
@Rule(RuleType.array().items(RuleType.string()).optional()) @Rule(RuleType.array().items(RuleType.string()).optional())
siteSkus?: string[]; siteSkus?: string[];
@ -164,10 +153,7 @@ export class UpdateProductDTO {
@Rule(RuleType.number()) @Rule(RuleType.number())
promotionPrice?: 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 }) @ApiProperty({ description: '属性列表', type: 'array', required: false })
@ -242,10 +228,6 @@ export class BatchUpdateProductDTO {
@Rule(RuleType.number().optional()) @Rule(RuleType.number().optional())
promotionPrice?: number; 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 }) @ApiProperty({ description: '属性列表', type: 'array', required: false })
@Rule(RuleType.array().optional()) @Rule(RuleType.array().optional())
attributes?: AttributeInputDTO[]; attributes?: AttributeInputDTO[];
@ -319,8 +301,6 @@ export interface ProductWhereFilter {
updatedAtStart?: string; updatedAtStart?: string;
// 更新时间范围结束 // 更新时间范围结束
updatedAtEnd?: string; updatedAtEnd?: string;
// TODO 使用 attributes 过滤
attributes?: Record<string, string>;
} }
/** /**

File diff suppressed because it is too large Load Diff

View File

@ -3,7 +3,6 @@ import {
UnifiedPaginationDTO, UnifiedPaginationDTO,
} from './api.dto'; } from './api.dto';
import { Dict } from '../entity/dict.entity'; import { Dict } from '../entity/dict.entity';
import { Product } from '../entity/product.entity';
// export class UnifiedOrderWhere{ // export class UnifiedOrderWhere{
// [] // []
// } // }
@ -19,24 +18,6 @@ export enum OrderFulfillmentStatus {
// 确认发货 // 确认发货
CONFIRMED, CONFIRMED,
} }
export enum OrderPaymentStatus {
// 待支付
PENDING,
// 支付中
PAYING,
// 部分支付
PARTIALLY_PAID,
// 已支付
PAID,
// 支付失败
FAILED,
// 部分退款
PARTIALLY_REFUNDED,
// 已退款
REFUNDED,
// 已取消
CANCELLED,
}
// //
export class UnifiedProductWhere { export class UnifiedProductWhere {
sku?: string; sku?: string;
@ -160,7 +141,7 @@ export class UnifiedProductAttributeDTO {
@ApiProperty({ description: '属性选项', type: [String] }) @ApiProperty({ description: '属性选项', type: [String] })
options: string[]; options: string[];
@ApiProperty({ description: '变体属性值(单个值)', required: false }) @ApiProperty({ description: '变体属性值(单个值)', required: false })
option?: string; option?: string;
// 这个是属性的父级字典项 // 这个是属性的父级字典项
dict?: Dict; dict?: Dict;
@ -307,7 +288,17 @@ export class UnifiedProductDTO {
type: 'object', type: 'object',
required: false, required: false,
}) })
erpProduct?: Product erpProduct?: {
id: number;
sku: string;
name: string;
nameCn?: string;
category?: any;
attributes?: any[];
components?: any[];
price: number;
promotionPrice: number;
};
} }
export class UnifiedOrderRefundDTO { export class UnifiedOrderRefundDTO {
@ -799,16 +790,14 @@ export class UpdateWebhookDTO {
export class FulfillmentItemDTO { export class FulfillmentItemDTO {
@ApiProperty({ description: '订单项ID' ,required: false}) @ApiProperty({ description: '订单项ID' })
order_item_id: number; order_item_id: number;
@ApiProperty({ description: '数量' ,required:false}) @ApiProperty({ description: '数量' })
quantity: number; quantity: number;
} }
export class FulfillmentDTO { export class FulfillmentDTO {
@ApiProperty({ description: '物流id', required: false })
tracking_id?: string;
@ApiProperty({ description: '物流单号', required: false }) @ApiProperty({ description: '物流单号', required: false })
tracking_number?: string; tracking_number?: string;

View File

@ -121,7 +121,7 @@ export class UpdateSiteDTO {
skuPrefix?: string; skuPrefix?: string;
// 区域 // 区域
@ApiProperty({ description: '区域', required: false }) @ApiProperty({ description: '区域' })
@Rule(RuleType.array().items(RuleType.string()).optional()) @Rule(RuleType.array().items(RuleType.string()).optional())
areas?: string[]; areas?: string[];
@ -133,10 +133,6 @@ export class UpdateSiteDTO {
@ApiProperty({ description: '站点网站URL', required: false }) @ApiProperty({ description: '站点网站URL', required: false })
@Rule(RuleType.string().optional()) @Rule(RuleType.string().optional())
websiteUrl?: string; websiteUrl?: string;
@ApiProperty({ description: 'Webhook URL', required: false })
@Rule(RuleType.string().optional())
webhookUrl?: string;
} }
export class QuerySiteDTO { export class QuerySiteDTO {
@ -156,7 +152,7 @@ export class QuerySiteDTO {
@Rule(RuleType.boolean().optional()) @Rule(RuleType.boolean().optional())
isDisabled?: boolean; isDisabled?: boolean;
@ApiProperty({ description: '站点ID列表(逗号分隔)', required: false }) @ApiProperty({ description: '站点ID列表(逗号分隔)', required: false })
@Rule(RuleType.string().optional()) @Rule(RuleType.string().optional())
ids?: string; ids?: string;
} }

View File

@ -19,10 +19,6 @@ export class OrderStatisticsParams {
@Rule(RuleType.number().allow(null)) @Rule(RuleType.number().allow(null))
siteId?: number; siteId?: number;
@ApiProperty()
@Rule(RuleType.array().allow(null))
country?: any[];
@ApiProperty({ @ApiProperty({
enum: ['all', 'first_purchase', 'repeat_purchase'], enum: ['all', 'first_purchase', 'repeat_purchase'],
default: 'all', default: 'all',

View File

@ -8,11 +8,11 @@ export interface WooProduct {
id: number; id: number;
// 创建时间 // 创建时间
date_created: string; date_created: string;
// 创建时间(GMT) // 创建时间GMT
date_created_gmt: string; date_created_gmt: string;
// 更新时间 // 更新时间
date_modified: string; date_modified: string;
// 更新时间(GMT) // 更新时间GMT
date_modified_gmt: string; date_modified_gmt: string;
// 产品类型 simple grouped external variable // 产品类型 simple grouped external variable
type: string; type: string;
@ -20,7 +20,7 @@ export interface WooProduct {
status: string; status: string;
// 是否为特色产品 // 是否为特色产品
featured: boolean; featured: boolean;
// 目录可见性选项:visible, catalog, search and hidden. Default is visible. // 目录可见性选项visible, catalog, search and hidden. Default is visible.
catalog_visibility: string; catalog_visibility: string;
// 常规价格 // 常规价格
@ -130,11 +130,11 @@ export interface WooVariation {
id: number; id: number;
// 创建时间 // 创建时间
date_created: string; date_created: string;
// 创建时间(GMT) // 创建时间GMT
date_created_gmt: string; date_created_gmt: string;
// 更新时间 // 更新时间
date_modified: string; date_modified: string;
// 更新时间(GMT) // 更新时间GMT
date_modified_gmt: string; date_modified_gmt: string;
// 变体描述 // 变体描述
description: string; description: string;
@ -150,11 +150,11 @@ export interface WooVariation {
price_html?: string; price_html?: string;
// 促销开始日期 // 促销开始日期
date_on_sale_from?: string; date_on_sale_from?: string;
// 促销开始日期(GMT) // 促销开始日期GMT
date_on_sale_from_gmt?: string; date_on_sale_from_gmt?: string;
// 促销结束日期 // 促销结束日期
date_on_sale_to?: string; date_on_sale_to?: string;
// 促销结束日期(GMT) // 促销结束日期GMT
date_on_sale_to_gmt?: string; date_on_sale_to_gmt?: string;
// 是否在促销中 // 是否在促销中
on_sale: boolean; on_sale: boolean;
@ -370,24 +370,17 @@ export interface WooOrder {
date_modified?: string; date_modified?: string;
date_modified_gmt?: string; date_modified_gmt?: string;
// 物流追踪信息 // 物流追踪信息
fulfillments?: WooFulfillment[]; fulfillments?: Array<{
} tracking_number?: string;
// 这个是一个插件的物流追踪信息 shipping_provider?: string;
// 接口: shipping_method?: string;
export interface WooFulfillment { status?: string;
data_sipped: string; date_created?: string;
tracking_id: string; items?: Array<{
tracking_link: string; order_item_id?: number;
tracking_number: string; quantity?: number;
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 { export interface WooOrderRefund {
id?: number; id?: number;
@ -559,8 +552,7 @@ export interface WooOrderSearchParams {
order: string; order: string;
orderby: string; orderby: string;
parant: string[]; parant: string[];
parent_exclude: string[]; status: (WooOrderStatusSearchParams)[];
status: WooOrderStatusSearchParams[];
customer: number; customer: number;
product: number; product: number;
dp: number; dp: number;
@ -624,83 +616,6 @@ export interface ListParams {
parant: string[]; parant: string[];
parent_exclude: 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 { export enum WooContext {
view, view,
edit edit

View File

@ -272,14 +272,6 @@ export class Order {
@Expose() @Expose()
updatedAt: Date; updatedAt: Date;
@ApiProperty({ type: 'json', nullable: true, description: '订单项列表' })
@Expose()
orderItems?: any[];
@ApiProperty({ type: 'json', nullable: true, description: '销售项列表' })
@Expose()
orderSales?: any[];
// 在插入或更新前处理用户代理字符串 // 在插入或更新前处理用户代理字符串
@BeforeInsert() @BeforeInsert()
@BeforeUpdate() @BeforeUpdate()

View File

@ -1,8 +1,8 @@
import { ApiProperty } from '@midwayjs/swagger'; import { ApiProperty } from '@midwayjs/swagger';
import { Exclude, Expose } from 'class-transformer'; import { Exclude, Expose } from 'class-transformer';
import { import {
// BeforeInsert, BeforeInsert,
// BeforeUpdate, BeforeUpdate,
Column, Column,
CreateDateColumn, CreateDateColumn,
Entity, Entity,
@ -22,31 +22,26 @@ export class OrderSale {
@Expose() @Expose()
id?: number; id?: number;
@ApiProperty({ name:'原始订单ID' }) @ApiProperty()
@Column() @Column()
@Expose() @Expose()
orderId: number; // 订单 ID orderId: number; // 订单 ID
@ApiProperty({ name:'站点' }) @ApiProperty()
@Column() @Column({ nullable: true })
@Expose() @Expose()
siteId: number; // 来源站点唯一标识 siteId: number; // 来源站点唯一标识
@ApiProperty({name: "原始订单 itemId"}) @ApiProperty()
@Column({ nullable: true }) @Column({ nullable: true })
@Expose() @Expose()
externalOrderItemId: string; // WooCommerce 订单item ID externalOrderItemId: string; // WooCommerce 订单item ID
@ApiProperty({name: "父产品 ID"}) @ApiProperty()
@Column({ nullable: true })
@Expose()
parentProductId?: number; // 父产品 ID 用于统计套餐 如果是单品则不记录
@ApiProperty({name: "产品 ID"})
@Column() @Column()
@Expose() @Expose()
productId: number; productId: number;
@ApiProperty() @ApiProperty()
@Column() @Column()
@Expose() @Expose()
@ -55,7 +50,7 @@ export class OrderSale {
@ApiProperty({ description: 'sku', type: 'string' }) @ApiProperty({ description: 'sku', type: 'string' })
@Expose() @Expose()
@Column() @Column()
sku: string;// 库存产品sku sku: string;
@ApiProperty() @ApiProperty()
@Column() @Column()
@ -67,40 +62,25 @@ export class OrderSale {
@Expose() @Expose()
isPackage: boolean; isPackage: boolean;
@ApiProperty({ description: '商品品类', type: 'string',nullable: true}) @ApiProperty()
@Column({ default: false })
@Expose() @Expose()
@Column({ nullable: true }) isYoone: boolean;
category?: string;
// TODO 这个其实还是直接保存 product 比较好
@ApiProperty({ description: '品牌', type: 'string',nullable: true})
@Expose()
@Column({ nullable: true })
brand?: string;
@ApiProperty({ description: '口味', type: 'string', nullable: true }) @ApiProperty()
@Column({ default: false })
@Expose() @Expose()
@Column({ nullable: true }) isZex: boolean;
flavor?: string;
@ApiProperty({ description: '湿度', type: 'string', nullable: true }) @ApiProperty({ nullable: true })
@Column({ type: 'int', nullable: true })
@Expose() @Expose()
@Column({ nullable: true }) size: number | null;
humidity?: string;
@ApiProperty({ description: '尺寸', type: 'string', nullable: true }) @ApiProperty()
@Column({ default: false })
@Expose() @Expose()
@Column({ nullable: true }) isYooneNew: boolean;
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({ @ApiProperty({
example: '2022-12-12 11:11:11', example: '2022-12-12 11:11:11',
@ -117,4 +97,25 @@ export class OrderSale {
@UpdateDateColumn() @UpdateDateColumn()
@Expose() @Expose()
updatedAt?: Date; 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;
}
} }

View File

@ -55,9 +55,6 @@ export class Product {
@Column({ nullable: true }) @Column({ nullable: true })
description?: string; description?: string;
@ApiProperty({ example: '图片URL', description: '产品图片URL' })
@Column({ nullable: true })
image?: string;
// 商品价格 // 商品价格
@ApiProperty({ description: '价格', example: 99.99 }) @ApiProperty({ description: '价格', example: 99.99 })
@Column({ type: 'decimal', precision: 10, scale: 2, default: 0 }) @Column({ type: 'decimal', precision: 10, scale: 2, default: 0 })
@ -73,10 +70,6 @@ export class Product {
@JoinColumn({ name: 'categoryId' }) @JoinColumn({ name: 'categoryId' })
category: Category; category: Category;
@ApiProperty({ description: '分类 ID', nullable: true, example: 1 })
@Column({ nullable: true })
categoryId?: number;
@ManyToMany(() => DictItem, dictItem => dictItem.products, { @ManyToMany(() => DictItem, dictItem => dictItem.products, {
cascade: true, cascade: true,
}) })

View File

@ -15,7 +15,7 @@ export class ProductStockComponent {
@ApiProperty({ description: '组件所关联的 SKU', type: 'string' }) @ApiProperty({ description: '组件所关联的 SKU', type: 'string' })
@Column({ type: 'varchar', length: 64 }) @Column({ type: 'varchar', length: 64 })
sku: string; sku: string;
@ApiProperty({ type: Number, description: '组成数量' }) @ApiProperty({ type: Number, description: '组成数量' })
@Column({ type: 'int', default: 1 }) @Column({ type: 'int', default: 1 })
quantity: number; quantity: number;

View File

@ -54,9 +54,9 @@ export class Shipment {
tracking_provider?: string; tracking_provider?: string;
@ApiProperty() @ApiProperty()
@Column({ nullable: true }) @Column()
@Expose() @Expose()
unique_id?: string; unique_id: string;
@Column({ nullable: true }) @Column({ nullable: true })
@Expose() @Expose()

View File

@ -47,11 +47,6 @@ export class ShippingAddress {
@Expose() @Expose()
phone_number_country: string; phone_number_country: string;
@ApiProperty()
@Column()
@Expose()
email: string;
@ApiProperty({ @ApiProperty({
example: '2022-12-12 11:11:11', example: '2022-12-12 11:11:11',
description: '创建时间', description: '创建时间',

View File

@ -1,86 +0,0 @@
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;
}

View File

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

View File

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

View File

@ -75,20 +75,17 @@ export class SyncUniuniShipmentJob implements IJob{
'255': 'Gateway_To_Gateway_Transit' '255': 'Gateway_To_Gateway_Transit'
}; };
async onTick() { async onTick() {
try {
const shipments:Shipment[] = await this.shipmentModel.findBy({ finished: false }); const shipments:Shipment[] = await this.shipmentModel.findBy({ finished: false });
const results = await Promise.all( shipments.forEach(shipment => {
shipments.map(async shipment => { this.logisticsService.updateShipmentState(shipment);
return await this.logisticsService.updateShipmentState(shipment); });
}) } catch (error) {
) this.logger.error(`更新运单状态失败 ${error.message}`);
this.logger.info(`更新运单状态完毕 ${JSON.stringify(results)}`); }
return results
} }
onComplete(result: any) { onComplete(result: any) {
this.logger.info(`更新运单状态完成 ${result}`);
}
onError(error: any) {
this.logger.error(`更新运单状态失败 ${error.message}`);
} }
} }

View File

@ -1,40 +0,0 @@
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}`);
}
}

View File

@ -21,8 +21,7 @@ export class CategoryService {
order: { order: {
sort: 'DESC', sort: 'DESC',
createdAt: 'DESC' createdAt: 'DESC'
}, }
relations: ['attributes', 'attributes.attributeDict']
}); });
} }

View File

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

View File

@ -50,7 +50,7 @@ export class DictService {
} }
// 从XLSX文件导入字典 // 从XLSX文件导入字典
async importDictsFromTable(bufferOrPath: Buffer | string) { async importDictsFromXLSX(bufferOrPath: Buffer | string) {
// 判断传入的是 Buffer 还是文件路径字符串 // 判断传入的是 Buffer 还是文件路径字符串
let buffer: Buffer; let buffer: Buffer;
if (typeof bufferOrPath === 'string') { if (typeof bufferOrPath === 'string') {
@ -60,7 +60,7 @@ export class DictService {
// 如果是 Buffer直接使用 // 如果是 Buffer直接使用
buffer = bufferOrPath; buffer = bufferOrPath;
} }
// 读取缓冲区中的工作簿 // 读取缓冲区中的工作簿
const wb = xlsx.read(buffer, { type: 'buffer' }); const wb = xlsx.read(buffer, { type: 'buffer' });
// 获取第一个工作表的名称 // 获取第一个工作表的名称
@ -93,7 +93,7 @@ export class DictService {
// 从XLSX文件导入字典项 // 从XLSX文件导入字典项
async importDictItemsFromXLSX(bufferOrPath: Buffer | string, dictId: number): Promise<BatchOperationResultDTO> { async importDictItemsFromXLSX(bufferOrPath: Buffer | string, dictId: number): Promise<BatchOperationResultDTO> {
if (!dictId) { if(!dictId){
throw new Error("引入失败, 请输入字典 ID") throw new Error("引入失败, 请输入字典 ID")
} }
@ -101,7 +101,7 @@ export class DictService {
if (!dict) { if (!dict) {
throw new Error('指定的字典不存在'); throw new Error('指定的字典不存在');
} }
// 判断传入的是 Buffer 还是文件路径字符串 // 判断传入的是 Buffer 还是文件路径字符串
let buffer: Buffer; let buffer: Buffer;
if (typeof bufferOrPath === 'string') { if (typeof bufferOrPath === 'string') {
@ -111,7 +111,7 @@ export class DictService {
// 如果是 Buffer直接使用 // 如果是 Buffer直接使用
buffer = bufferOrPath; buffer = bufferOrPath;
} }
const wb = xlsx.read(buffer, { type: 'buffer' }); const wb = xlsx.read(buffer, { type: 'buffer' });
const wsname = wb.SheetNames[0]; const wsname = wb.SheetNames[0];
const ws = wb.Sheets[wsname]; const ws = wb.Sheets[wsname];
@ -122,7 +122,7 @@ export class DictService {
const createdItems = []; const createdItems = [];
const updatedItems = []; const updatedItems = [];
const errors = []; const errors = [];
for (const row of data) { for (const row of data) {
try { try {
const result = await this.upsertDictItem(dictId, { const result = await this.upsertDictItem(dictId, {
@ -150,7 +150,7 @@ export class DictService {
const processed = createdItems.length + updatedItems.length; const processed = createdItems.length + updatedItems.length;
return { return {
total: data.length, total: data.length,
processed: processed, processed: processed,
updated: updatedItems.length, updated: updatedItems.length,
@ -216,10 +216,10 @@ export class DictService {
// 如果提供了 dictId,则只返回该字典下的项 // 如果提供了 dictId,则只返回该字典下的项
if (params.dictId) { if (params.dictId) {
return this.dictItemModel.find({ where, relations: ['dict'] }); return this.dictItemModel.find({ where });
} }
// 否则,返回所有字典项 // 否则,返回所有字典项
return this.dictItemModel.find({ relations: ['dict'] }); return this.dictItemModel.find();
} }
// 创建新字典项 // 创建新字典项
@ -239,7 +239,7 @@ export class DictService {
} }
// 更新或创建字典项 (Upsert) // 更新或创建字典项 (Upsert)
// 如果字典项已存在(根据 name 和 dictId 判断),则更新;否则创建新的 // 如果字典项已存在(根据 name 和 dictId 判断),则更新;否则创建新的
async upsertDictItem(dictId: number, itemData: { async upsertDictItem(dictId: number, itemData: {
name: string; name: string;
title: string; title: string;
@ -251,8 +251,8 @@ export class DictService {
}) { }) {
// 格式化 name // 格式化 name
const formattedName = this.formatName(itemData.name); const formattedName = this.formatName(itemData.name);
// 查找是否已存在该字典项(根据 name 和 dictId) // 查找是否已存在该字典项(根据 name 和 dictId
const existingItem = await this.dictItemModel.findOne({ const existingItem = await this.dictItemModel.findOne({
where: { where: {
name: formattedName, name: formattedName,

View File

@ -1,332 +0,0 @@
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);
}
}
}
}

View File

@ -27,12 +27,10 @@ import { CanadaPostService } from './canadaPost.service';
import { OrderItem } from '../entity/order_item.entity'; import { OrderItem } from '../entity/order_item.entity';
import { OrderSale } from '../entity/order_sale.entity'; import { OrderSale } from '../entity/order_sale.entity';
import { UniExpressService } from './uni_express.service'; import { UniExpressService } from './uni_express.service';
import { FreightwavesService, RateTryRequest } from './freightwaves.service';
import { StockPoint } from '../entity/stock_point.entity'; import { StockPoint } from '../entity/stock_point.entity';
import { OrderService } from './order.service'; import { OrderService } from './order.service';
import { convertKeysFromCamelToSnake } from '../utils/object-transform.util'; import { convertKeysFromCamelToSnake } from '../utils/object-transform.util';
import { SiteService } from './site.service'; import { SiteService } from './site.service';
import { ShopyyService } from './shopyy.service';
@Provide() @Provide()
export class LogisticsService { export class LogisticsService {
@ -75,15 +73,9 @@ export class LogisticsService {
@Inject() @Inject()
uniExpressService: UniExpressService; uniExpressService: UniExpressService;
@Inject()
freightwavesService: FreightwavesService;
@Inject() @Inject()
wpService: WPService; wpService: WPService;
@Inject()
shopyyService: ShopyyService;
@Inject() @Inject()
orderService: OrderService; orderService: OrderService;
@ -133,10 +125,6 @@ export class LogisticsService {
try { try {
const data = await this.uniExpressService.getOrderStatus(shipment.return_tracking_number); const data = await this.uniExpressService.getOrderStatus(shipment.return_tracking_number);
console.log('updateShipmentState data:', data); console.log('updateShipmentState data:', data);
// huo
if (data.status === 'FAIL') {
throw new Error('获取运单状态失败,原因为' + data.ret_msg)
}
shipment.state = data.data[0].state; shipment.state = data.data[0].state;
if (shipment.state in [203, 215, 216, 230]) { // todo,写常数 if (shipment.state in [203, 215, 216, 230]) { // todo,写常数
shipment.finished = true; shipment.finished = true;
@ -149,30 +137,6 @@ 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) { async updateShipmentStateById(id: number) {
const shipment: Shipment = await this.shipmentModel.findOneBy({ id: id }); const shipment: Shipment = await this.shipmentModel.findOneBy({ id: id });
return this.updateShipmentState(shipment); return this.updateShipmentState(shipment);
@ -279,7 +243,8 @@ export class LogisticsService {
shipmentRepo.remove(shipment); shipmentRepo.remove(shipment);
await this.uniExpressService.deleteShipment(shipment.return_tracking_number); const res = await this.uniExpressService.deleteShipment(shipment.return_tracking_number);
console.log('res', res.data); // todo
await orderRepo.save(order); await orderRepo.save(order);
@ -309,6 +274,7 @@ export class LogisticsService {
console.log('同步到woocommerce失败', error); console.log('同步到woocommerce失败', error);
return true; return true;
} }
return true; return true;
} catch { } catch {
throw new Error('删除运单失败'); throw new Error('删除运单失败');
@ -324,16 +290,7 @@ export class LogisticsService {
currency: 'CAD', currency: 'CAD',
// item_description: data.sales, // todo: 货品信息 // item_description: data.sales, // todo: 货品信息
} }
let resShipmentFee: any; const resShipmentFee = await this.uniExpressService.getRates(reqBody);
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') { if (resShipmentFee.status !== 'SUCCESS') {
throw new Error(resShipmentFee.ret_msg); throw new Error(resShipmentFee.ret_msg);
} }
@ -358,10 +315,40 @@ export class LogisticsService {
let resShipmentOrder; let resShipmentOrder;
try { try {
resShipmentOrder = await this.mepShipment(data, order); 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
}
}
// 记录物流信息,并将订单状态转到完成,uniuni状态为SUCCESStms.freightwaves状态为00000200 // 添加运单
if (resShipmentOrder.status === 'SUCCESS' || resShipmentOrder.code === '00000200') { resShipmentOrder = await this.uniExpressService.createShipment(reqBody);
// 记录物流信息,并将订单状态转到完成
if (resShipmentOrder.status === 'SUCCESS') {
order.orderStatus = ErpOrderStatus.COMPLETED; order.orderStatus = ErpOrderStatus.COMPLETED;
} else { } else {
throw new Error('运单生成失败'); throw new Error('运单生成失败');
@ -372,89 +359,49 @@ export class LogisticsService {
await dataSource.transaction(async manager => { await dataSource.transaction(async manager => {
const orderRepo = manager.getRepository(Order); const orderRepo = manager.getRepository(Order);
const shipmentRepo = manager.getRepository(Shipment); const shipmentRepo = manager.getRepository(Shipment);
const tracking_provider = data.shipmentPlatform; // todo: id未确定,后写进常数 const tracking_provider = 'UniUni'; // todo: id未确定,后写进常数
// 同步物流信息到woocommerce // 同步物流信息到woocommerce
const site = await this.siteService.get(Number(order.siteId), true); const site = await this.siteService.get(Number(order.siteId), true);
let co: any; const res = await this.wpService.createFulfillment(site, order.externalOrderId, {
let unique_id: any; tracking_number: resShipmentOrder.data.tno,
let state: any; tracking_provider: tracking_provider,
if (data.shipmentPlatform === 'uniuni') { });
co = resShipmentOrder.data.tno;
unique_id = resShipmentOrder.data.uni_order_sn; if (order.orderStatus === ErpOrderStatus.COMPLETED) {
state = resShipmentOrder.data.uni_status_code; const shipment = await shipmentRepo.save({
} else { tracking_provider: tracking_provider,
co = resShipmentOrder.data?.shipOrderId; tracking_id: res.data.tracking_id,
unique_id = resShipmentOrder.data?.shipOrderId; unique_id: resShipmentOrder.data.uni_order_sn,
state = ErpOrderStatus.COMPLETED; 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;
} }
// 同步订单状态到woocommerce // 同步订单状态到woocommerce
if (order.source_type != "shopyy") { if (order.status !== OrderStatus.COMPLETED) {
const res = await this.wpService.createFulfillment(site, order.externalOrderId, { await this.wpService.updateOrder(site, order.externalOrderId, {
tracking_number: co, status: OrderStatus.COMPLETED,
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; order.orderStatus = ErpOrderStatus.COMPLETED;
await orderRepo.save(order); await orderRepo.save(order);
}).catch(error => { }).catch(error => {
transactionError = error transactionError = error
throw new Error(`请求错误:${error}`);
}); });
if (transactionError !== undefined) { if (transactionError !== undefined) {
console.log('err', transactionError);
throw transactionError; throw transactionError;
} }
// 更新产品发货信息 // 更新产品发货信息
this.orderService.updateOrderSales(order.id, sales); this.orderService.updateOrderSales(order.id, sales);
@ -691,190 +638,4 @@ export class LogisticsService {
return { items, total, current, pageSize }; 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: '', // 详细地址2Address类型中没有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: '', // 详细地址2Address类型中没有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;
}
} }

View File

@ -39,7 +39,6 @@ import * as path from 'path';
import * as os from 'os'; import * as os from 'os';
import { UnifiedOrderDTO } from '../dto/site-api.dto'; import { UnifiedOrderDTO } from '../dto/site-api.dto';
import { CustomerService } from './customer.service'; import { CustomerService } from './customer.service';
import { ProductService } from './product.service';
@Provide() @Provide()
export class OrderService { export class OrderService {
@ -111,9 +110,7 @@ export class OrderService {
@Logger() @Logger()
logger; // 注入 Logger 实例 logger; // 注入 Logger 实例
@Inject()
productService: ProductService;
/** /**
* *
* : * :
@ -141,7 +138,7 @@ export class OrderService {
updated: 0, updated: 0,
errors: [] errors: []
}; };
this.logger.info('开始进入循环同步订单', result.length, '个订单')
// 遍历每个订单进行同步 // 遍历每个订单进行同步
for (const order of result) { for (const order of result) {
try { try {
@ -149,8 +146,8 @@ export class OrderService {
const existingOrder = await this.orderModel.findOne({ const existingOrder = await this.orderModel.findOne({
where: { externalOrderId: String(order.id), siteId: siteId }, where: { externalOrderId: String(order.id), siteId: siteId },
}); });
if (!existingOrder) { if(!existingOrder){
this.logger.debug("数据库中不存在", order.id, '订单状态:', order.status) console.log("数据库中不存在",order.id, '订单状态:', order.status )
} }
// 同步单个订单 // 同步单个订单
await this.syncSingleOrder(siteId, order); await this.syncSingleOrder(siteId, order);
@ -165,7 +162,6 @@ export class OrderService {
} else { } else {
syncResult.created++; syncResult.created++;
} }
// console.log('updated', syncResult.updated, 'created:', syncResult.created)
} catch (error) { } catch (error) {
// 记录错误但不中断整个同步过程 // 记录错误但不中断整个同步过程
syncResult.errors.push({ syncResult.errors.push({
@ -175,7 +171,7 @@ export class OrderService {
syncResult.processed++; syncResult.processed++;
} }
} }
this.logger.info('同步完成', syncResult.updated, 'created:', syncResult.created) this.logger.debug('syncOrders result', syncResult)
return syncResult; return syncResult;
} }
@ -212,8 +208,8 @@ export class OrderService {
const existingOrder = await this.orderModel.findOne({ const existingOrder = await this.orderModel.findOne({
where: { externalOrderId: String(order.id), siteId: siteId }, where: { externalOrderId: String(order.id), siteId: siteId },
}); });
if (!existingOrder) { if(!existingOrder){
this.logger.debug("数据库不存在", siteId, "订单:", order.id, '订单状态:' + order.status) console.log("数据库不存在", siteId , "订单:",order.id, '订单状态:' + order.status )
} }
// 同步单个订单 // 同步单个订单
await this.syncSingleOrder(siteId, order, true); await this.syncSingleOrder(siteId, order, true);
@ -272,7 +268,7 @@ export class OrderService {
try { try {
const site = await this.siteService.get(siteId); const site = await this.siteService.get(siteId);
// 仅处理 WooCommerce 站点 // 仅处理 WooCommerce 站点
if (site.type !== 'woocommerce') { if(site.type !== 'woocommerce'){
return return
} }
// 将订单状态同步到 WooCommerce,然后切换至下一状态 // 将订单状态同步到 WooCommerce,然后切换至下一状态
@ -282,11 +278,6 @@ export class OrderService {
console.error('更新订单状态失败,原因为:', error) console.error('更新订单状态失败,原因为:', error)
} }
} }
async getOrderByExternalOrderId(siteId: number, externalOrderId: string) {
return await this.orderModel.findOne({
where: { externalOrderId: String(externalOrderId), siteId },
});
}
/** /**
* *
* : * :
@ -310,7 +301,7 @@ export class OrderService {
* @param order * @param order
* @param forceUpdate * @param forceUpdate
*/ */
async syncSingleOrder(siteId: number, order: UnifiedOrderDTO, forceUpdate = false) { async syncSingleOrder(siteId: number, order: any, forceUpdate = false) {
// 从订单数据中解构出各个子项 // 从订单数据中解构出各个子项
let { let {
line_items, line_items,
@ -324,50 +315,53 @@ export class OrderService {
// console.log('同步进单个订单', order) // console.log('同步进单个订单', order)
// 如果订单状态为 AUTO_DRAFT,则跳过处理 // 如果订单状态为 AUTO_DRAFT,则跳过处理
if (order.status === OrderStatus.AUTO_DRAFT) { if (order.status === OrderStatus.AUTO_DRAFT) {
this.logger.debug('订单状态为 AUTO_DRAFT,跳过处理', siteId, order.id)
return; return;
} }
// 检查数据库中是否已存在该订单 // 检查数据库中是否已存在该订单
const existingOrder = await this.orderModel.findOne({ const existingOrder = await this.orderModel.findOne({
where: { externalOrderId: String(order.id), siteId: siteId }, where: { externalOrderId: order.id, siteId: siteId },
}); });
// 自动更新订单状态(如果需要) // 自动更新订单状态(如果需要)
await this.autoUpdateOrderStatus(siteId, order); await this.autoUpdateOrderStatus(siteId, order);
if (existingOrder) { if(existingOrder){
// 矫正数据库中的订单数据 // 矫正数据库中的订单数据
const updateData: any = { status: order.status }; const updateData: any = { status: order.status };
if (this.canUpdateErpStatus(existingOrder.orderStatus)) { if (this.canUpdateErpStatus(existingOrder.orderStatus)) {
updateData.orderStatus = this.mapOrderStatus(order.status as any); updateData.orderStatus = this.mapOrderStatus(order.status);
} }
// 更新订单主数据 // 更新
await this.orderModel.update({ externalOrderId: String(order.id), siteId: siteId }, updateData); await this.orderModel.update({ externalOrderId: order.id, siteId: siteId }, updateData);
// 更新 fulfillments 数据 // 更新 fulfillments 数据
await this.saveOrderFulfillments({ await this.saveOrderFulfillments({
siteId, siteId,
orderId: existingOrder.id, orderId: existingOrder.id,
externalOrderId: order.id, externalOrderId:order.id,
fulfillments: fulfillments, fulfillments: fulfillments,
}); });
} }
const externalOrderId = String(order.id); const externalOrderId = order.id;
// 这里的 saveOrder 已经包括了创建订单和更新订单 // 如果订单从未完成变为完成状态,则更新库存
let orderRecord: Order = await this.saveOrder(siteId, orderData);
// 如果订单从未完成变为完成状态,则更新库存
if ( if (
orderRecord && existingOrder &&
orderRecord.orderStatus !== ErpOrderStatus.COMPLETED && existingOrder.orderStatus !== ErpOrderStatus.COMPLETED &&
orderData.status === OrderStatus.COMPLETED orderData.status === OrderStatus.COMPLETED
) { ) {
await this.updateStock(orderRecord); this.updateStock(existingOrder);
// 不再直接返回,继续执行后续的更新操作 // 不再直接返回,继续执行后续的更新操作
} }
// 如果订单不可编辑且不强制更新,则跳过处理
if (existingOrder && !existingOrder.is_editable && !forceUpdate) {
return;
}
// 保存订单主数据
const orderRecord = await this.saveOrder(siteId, orderData);
const orderId = orderRecord.id; const orderId = orderRecord.id;
// 保存订单项 // 保存订单项
await this.saveOrderItems({ await this.saveOrderItems({
siteId, siteId,
orderId, orderId,
externalOrderId: String(externalOrderId), externalOrderId,
orderItems: line_items, orderItems: line_items,
}); });
// 保存退款信息 // 保存退款信息
@ -465,14 +459,13 @@ export class OrderService {
* @param order * @param order
* @returns * @returns
*/ */
// 这里 omit 是因为处理在外头了 其实 saveOrder 应该包括 savelineitems 等 async saveOrder(siteId: number, order: UnifiedOrderDTO): Promise<Order> {
async saveOrder(siteId: number, order: Omit<UnifiedOrderDTO, 'line_items' | 'refunds'>): Promise<Order> {
// 将外部订单ID转换为字符串 // 将外部订单ID转换为字符串
const externalOrderId = String(order.id) const externalOrderId = String(order.id)
delete 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({ const existingOrder = await this.orderModel.findOne({
where: { externalOrderId, siteId: siteId }, where: { externalOrderId, siteId: siteId },
@ -486,7 +479,7 @@ export class OrderService {
// 如果不能更新 ERP 状态,则保留原有的 orderStatus // 如果不能更新 ERP 状态,则保留原有的 orderStatus
entity.orderStatus = existingOrder.orderStatus; entity.orderStatus = existingOrder.orderStatus;
} }
// 更新订单数据(包括 shipping、billing 等字段) // 更新订单数据(包括 shipping、billing 等字段)
await this.orderModel.update(existingOrder.id, entity); await this.orderModel.update(existingOrder.id, entity);
entity.id = existingOrder.id; entity.id = existingOrder.id;
return entity; return entity;
@ -715,8 +708,6 @@ export class OrderService {
* *
* @param orderItem * @param orderItem
*/ */
// TODO 这里存的是库存商品实际
// 所以叫做 orderInventoryItems 可能更合适
async saveOrderSale(orderItem: OrderItem) { async saveOrderSale(orderItem: OrderItem) {
const currentOrderSale = await this.orderSaleModel.find({ const currentOrderSale = await this.orderSaleModel.find({
where: { where: {
@ -728,56 +719,53 @@ export class OrderService {
await this.orderSaleModel.delete(currentOrderSale.map(v => v.id)); await this.orderSaleModel.delete(currentOrderSale.map(v => v.id));
} }
if (!orderItem.sku) return; if (!orderItem.sku) return;
// 从数据库查询产品,关联查询组件 // 从数据库查询产品,关联查询组件
const productDetail = await this.productService.getComponentDetailFromSiteSku({ sku: orderItem.sku, name: orderItem.name }); const product = await this.productModel.findOne({
where: { siteSkus: Like(`%${orderItem.sku}%`) },
relations: ['components'],
});
if (!productDetail || !productDetail.quantity) return; if (!product) return;
const { product, quantity } = productDetail
const componentDetails: { product: Product, quantity: number }[] = product.components?.length > 0 ? await Promise.all(product.components.map(async comp => { const orderSales: OrderSale[] = [];
return {
product: await this.productModel.findOne({ if (product.components && product.components.length > 0) {
where: { id: comp.productId }, for (const comp of product.components) {
}), const baseProduct = await this.productModel.findOne({
quantity: comp.quantity * orderItem.quantity, 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);
}
} }
})) : [{ product, quantity }] } else {
const orderSaleItem: OrderSale = plainToClass(OrderSale, {
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, orderId: orderItem.orderId,
siteId: orderItem.siteId, siteId: orderItem.siteId,
externalOrderItemId: orderItem.externalOrderItemId,// 原始 itemId externalOrderItemId: orderItem.externalOrderItemId,
parentProductId: product.id, // 父产品 ID 用于统计套餐 如果是单品则不记录 productId: product.id,
productId: componentDetail.product.id, name: product.name,
isPackage: product.type === 'bundle',// 这里是否是套餐取决于父产品 quantity: orderItem.quantity,
name: componentDetail.product.name, sku: product.sku,
quantity: componentDetail.quantity * orderItem.quantity, isPackage: orderItem.name.toLowerCase().includes('package'),
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,
}); });
return orderSale orderSales.push(orderSaleItem);
}).filter(v => v !== null) }
if (orderSales.length > 0) { if (orderSales.length > 0) {
await this.orderSaleModel.save(orderSales); 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;
// }
/** /**
* 退 * 退
@ -1246,13 +1234,13 @@ export class OrderService {
parameters.push(siteId); parameters.push(siteId);
} }
if (startDate) { if (startDate) {
sqlQuery += ` AND o.date_paid >= ?`; sqlQuery += ` AND o.date_created >= ?`;
totalQuery += ` AND o.date_paid >= ?`; totalQuery += ` AND o.date_created >= ?`;
parameters.push(startDate); parameters.push(startDate);
} }
if (endDate) { if (endDate) {
sqlQuery += ` AND o.date_paid <= ?`; sqlQuery += ` AND o.date_created <= ?`;
totalQuery += ` AND o.date_paid <= ?`; totalQuery += ` AND o.date_created <= ?`;
parameters.push(endDate); parameters.push(endDate);
} }
// 支付方式筛选(使用参数化,避免SQL注入) // 支付方式筛选(使用参数化,避免SQL注入)
@ -1340,7 +1328,7 @@ export class OrderService {
// 添加分页到主查询 // 添加分页到主查询
sqlQuery += ` sqlQuery += `
GROUP BY o.id GROUP BY o.id
ORDER BY o.date_paid DESC ORDER BY o.date_created DESC
LIMIT ? OFFSET ? LIMIT ? OFFSET ?
`; `;
parameters.push(pageSize, (current - 1) * pageSize); parameters.push(pageSize, (current - 1) * pageSize);
@ -1438,7 +1426,7 @@ export class OrderService {
* @param params * @param params
* @returns * @returns
*/ */
async getOrderSales({ siteId, startDate, endDate, current, pageSize, name, exceptPackage, orderBy }: QueryOrderSalesDTO) { async getOrderSales({ siteId, startDate, endDate, current, pageSize, name, exceptPackage }: QueryOrderSalesDTO) {
const nameKeywords = name ? name.split(' ').filter(Boolean) : []; const nameKeywords = name ? name.split(' ').filter(Boolean) : [];
const defaultStart = dayjs().subtract(30, 'day').startOf('day').format('YYYY-MM-DD HH:mm:ss'); 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'); const defaultEnd = dayjs().endOf('day').format('YYYY-MM-DD HH:mm:ss');
@ -1558,6 +1546,7 @@ export class OrderService {
GROUP BY os.productId GROUP BY os.productId
`; `;
console.log('------3.5-----', pcSql, pcParams, exceptPackage);
const pcResults = await this.orderSaleModel.query(pcSql, pcParams); const pcResults = await this.orderSaleModel.query(pcSql, pcParams);
const pcMap = new Map<number, any>(); const pcMap = new Map<number, any>();
@ -1590,14 +1579,14 @@ export class OrderService {
`; `;
let yooneSql = ` let yooneSql = `
SELECT SELECT
SUM(CASE WHEN os.brand = 'yoone' AND os.strength = '3mg' THEN os.quantity ELSE 0 END) AS yoone3Quantity, SUM(CASE WHEN os.isYoone = 1 AND os.size = 3 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.isYoone = 1 AND os.size = 6 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.isYoone = 1 AND os.size = 9 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.isYoone = 1 AND os.size = 12 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.isYooneNew = 1 AND os.size = 12 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.isYoone = 1 AND os.size = 15 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.isYoone = 1 AND os.size = 18 THEN os.quantity ELSE 0 END) AS yoone18Quantity,
SUM(CASE WHEN os.brand = 'zex' THEN os.quantity ELSE 0 END) AS zexQuantity SUM(CASE WHEN os.isZex = 1 THEN os.quantity ELSE 0 END) AS zexQuantity
FROM order_sale os FROM order_sale os
INNER JOIN \`order\` o ON o.id = os.orderId INNER JOIN \`order\` o ON o.id = os.orderId
WHERE o.date_paid BETWEEN ? AND ? WHERE o.date_paid BETWEEN ? AND ?
@ -1653,12 +1642,11 @@ export class OrderService {
* @returns * @returns
*/ */
async getOrderItems({ async getOrderItems({
current,
pageSize,
siteId, siteId,
startDate, startDate,
endDate, endDate,
sku, current,
pageSize,
name, name,
}: QueryOrderSalesDTO) { }: QueryOrderSalesDTO) {
const nameKeywords = name ? name.split(' ').filter(Boolean) : []; const nameKeywords = name ? name.split(' ').filter(Boolean) : [];
@ -1916,8 +1904,8 @@ export class OrderService {
const key = it?.externalSubscriptionId const key = it?.externalSubscriptionId
? `sub:${it.externalSubscriptionId}` ? `sub:${it.externalSubscriptionId}`
: it?.externalOrderId : it?.externalOrderId
? `ord:${it.externalOrderId}` ? `ord:${it.externalOrderId}`
: `id:${it?.id}`; : `id:${it?.id}`;
if (!seen.has(key)) { if (!seen.has(key)) {
seen.add(key); seen.add(key);
relatedList.push(it); relatedList.push(it);
@ -2211,14 +2199,14 @@ export class OrderService {
for (const sale of sales) { for (const sale of sales) {
const product = await productRepo.findOne({ where: { sku: sale.sku } }); const product = await productRepo.findOne({ where: { sku: sale.sku } });
const saleItem = { const saleItem = {
orderId: order.id, orderId: order.id,
siteId: order.siteId, siteId: order.siteId,
externalOrderItemId: '-1', externalOrderItemId: '-1',
productId: product.id, productId: product.id,
name: product.name, name: product.name,
sku: sale.sku, sku: sale.sku,
quantity: sale.quantity, quantity: sale.quantity,
}; };
await orderSaleRepo.save(saleItem); await orderSaleRepo.save(saleItem);
} }
}); });
@ -2351,83 +2339,83 @@ export class OrderService {
//换货功能更新OrderSale和Orderitem数据 //换货功能更新OrderSale和Orderitem数据
async updateExchangeOrder(orderId: number, data: any) { async updateExchangeOrder(orderId: number, data: any) {
throw new Error('暂未实现') throw new Error('暂未实现')
// try { // try {
// const dataSource = this.dataSourceManager.getDataSource('default'); // const dataSource = this.dataSourceManager.getDataSource('default');
// let transactionError = undefined; // let transactionError = undefined;
// await dataSource.transaction(async manager => { // await dataSource.transaction(async manager => {
// const orderRepo = manager.getRepository(Order); // const orderRepo = manager.getRepository(Order);
// const orderSaleRepo = manager.getRepository(OrderSale); // const orderSaleRepo = manager.getRepository(OrderSale);
// const orderItemRepo = manager.getRepository(OrderItem); // const orderItemRepo = manager.getRepository(OrderItem);
// const productRepo = manager.getRepository(ProductV2); // const productRepo = manager.getRepository(ProductV2);
// const order = await orderRepo.findOneBy({ id: orderId }); // const order = await orderRepo.findOneBy({ id: orderId });
// let product: ProductV2; // let product: ProductV2;
// await orderSaleRepo.delete({ orderId }); // await orderSaleRepo.delete({ orderId });
// await orderItemRepo.delete({ orderId }); // await orderItemRepo.delete({ orderId });
// for (const sale of data['sales']) { // for (const sale of data['sales']) {
// product = await productRepo.findOneBy({ sku: sale['sku'] }); // product = await productRepo.findOneBy({ sku: sale['sku'] });
// await orderSaleRepo.save({ // await orderSaleRepo.save({
// orderId, // orderId,
// siteId: order.siteId, // siteId: order.siteId,
// productId: product.id, // productId: product.id,
// name: product.name, // name: product.name,
// sku: sale['sku'], // sku: sale['sku'],
// quantity: sale['quantity'], // quantity: sale['quantity'],
// }); // });
// }; // };
// for (const item of data['items']) { // for (const item of data['items']) {
// product = await productRepo.findOneBy({ sku: item['sku'] }); // product = await productRepo.findOneBy({ sku: item['sku'] });
// await orderItemRepo.save({ // await orderItemRepo.save({
// orderId, // orderId,
// siteId: order.siteId, // siteId: order.siteId,
// productId: product.id, // productId: product.id,
// name: product.name, // name: product.name,
// externalOrderId: order.externalOrderId, // externalOrderId: order.externalOrderId,
// externalProductId: product.externalProductId, // externalProductId: product.externalProductId,
// sku: item['sku'], // sku: item['sku'],
// quantity: item['quantity'], // quantity: item['quantity'],
// }); // });
// }; // };
// //将是否换货状态改为true // //将是否换货状态改为true
// await orderRepo.update( // await orderRepo.update(
// order.id // order.id
// , { // , {
// is_exchange: true // is_exchange: true
// }); // });
// //查询这个用户换过多少次货 // //查询这个用户换过多少次货
// const counts = await orderRepo.countBy({ // const counts = await orderRepo.countBy({
// is_editable: true, // is_editable: true,
// customer_email: order.customer_email, // customer_email: order.customer_email,
// }); // });
// //批量更新当前用户换货次数 // //批量更新当前用户换货次数
// await orderRepo.update({ // await orderRepo.update({
// customer_email: order.customer_email // customer_email: order.customer_email
// }, { // }, {
// exchange_frequency: counts // exchange_frequency: counts
// }); // });
// }).catch(error => { // }).catch(error => {
// transactionError = error; // transactionError = error;
// }); // });
// if (transactionError !== undefined) { // if (transactionError !== undefined) {
// throw new Error(`更新物流信息错误:${transactionError.message}`); // throw new Error(`更新物流信息错误:${transactionError.message}`);
// } // }
// return true; // return true;
// } catch (error) { // } catch (error) {
// throw new Error(`更新发货产品失败:${error.message}`); // throw new Error(`更新发货产品失败:${error.message}`);
// } // }
} }
/** /**
@ -2473,17 +2461,17 @@ export class OrderService {
} }
try { try {
// 过滤掉NaN和非数字值只保留有效的数字ID // 过滤掉NaN和非数字值只保留有效的数字ID
const validIds = ids?.filter?.(id => Number.isFinite(id) && id > 0); const validIds = ids?.filter?.(id => Number.isFinite(id) && id > 0);
const dataSource = this.dataSourceManager.getDataSource('default'); const dataSource = this.dataSourceManager.getDataSource('default');
// 优化事务使用 // 优化事务使用
return await dataSource.transaction(async manager => { return await dataSource.transaction(async manager => {
// 准备查询条件 // 准备查询条件
const whereCondition: any = {}; const whereCondition: any = {};
if (validIds.length > 0) { if(validIds.length > 0){
whereCondition.id = In(validIds); whereCondition.id = In(validIds);
} }
@ -2499,7 +2487,7 @@ export class OrderService {
// 获取所有订单ID // 获取所有订单ID
const orderIds = orders.map(order => order.id); const orderIds = orders.map(order => order.id);
// 获取所有订单项 // 获取所有订单项
const orderItems = await manager.getRepository(OrderItem).find({ const orderItems = await manager.getRepository(OrderItem).find({
where: { where: {
@ -2520,13 +2508,13 @@ export class OrderService {
const exportDataList: ExportData[] = orders.map(order => { const exportDataList: ExportData[] = orders.map(order => {
// 获取订单的订单项 // 获取订单的订单项
const items = orderItemsByOrderId[order.id] || []; const items = orderItemsByOrderId[order.id] || [];
// 计算总盒数 // 计算总盒数
const boxCount = items.reduce((total, item) => total + item.quantity, 0); const boxCount = items.reduce((total, item) => total + item.quantity, 0);
// 构建订单内容 // 构建订单内容
const orderContent = items.map(item => `${item.name} x ${item.quantity}`).join('; '); const orderContent = items.map(item => `${item.name} (${item.sku || ''}) x ${item.quantity}`).join('; ');
// 构建姓名地址 // 构建姓名地址
const shipping = order.shipping; const shipping = order.shipping;
const billing = order.billing; const billing = order.billing;
@ -2540,10 +2528,10 @@ export class OrderService {
const postcode = shipping?.postcode || billing?.postcode || ''; const postcode = shipping?.postcode || billing?.postcode || '';
const country = shipping?.country || billing?.country || ''; const country = shipping?.country || billing?.country || '';
const nameAddress = `${name} ${address} ${address2} ${city} ${state} ${postcode} ${country}`; const nameAddress = `${name} ${address} ${address2} ${city} ${state} ${postcode} ${country}`;
// 获取电话号码 // 获取电话号码
const phone = shipping?.phone || billing?.phone || ''; const phone = shipping?.phone || billing?.phone || '';
// 获取快递号 // 获取快递号
const trackingNumber = order.shipment?.tracking_id || ''; const trackingNumber = order.shipment?.tracking_id || '';
@ -2579,153 +2567,85 @@ export class OrderService {
* CSV格式 * CSV格式
* @param {any[]} data * @param {any[]} data
* @param {Object} options * @param {Object} options
* @param {string} [options.type='string'] :'string' | 'buffer' * @param {string} [options.type='string'] 'string' | 'buffer'
* @param {string} [options.fileName] (使) * @param {string} [options.fileName] 使
* @param {boolean} [options.writeFile=false] * @param {boolean} [options.writeFile=false]
* @returns {string|Buffer} type返回字符串或Buffer * @returns {string|Buffer} type返回字符串或Buffer
*/ */
async exportToCsv(data: any[], options: { type?: 'string' | 'buffer'; fileName?: string; writeFile?: boolean } = {}): Promise<string | Buffer> { async exportToCsv(data: any[], options: { type?: 'string' | 'buffer'; fileName?: string; writeFile?: boolean } = {}): Promise<string | Buffer> {
try { try {
// 检查数据是否为空 // 检查数据是否为空
if (!data || data.length === 0) { if (!data || data.length === 0) {
throw new Error('导出数据不能为空'); throw new Error('导出数据不能为空');
} }
const { type = 'string', fileName, writeFile = false } = options; const { type = 'string', fileName, writeFile = false } = options;
// 生成表头 // 生成表头
const headers = Object.keys(data[0]); const headers = Object.keys(data[0]);
let csvContent = headers.join(',') + '\n'; let csvContent = headers.join(',') + '\n';
// 处理数据行 // 处理数据行
data.forEach(item => { data.forEach(item => {
const row = headers.map(key => { const row = headers.map(key => {
const value = item[key as keyof any]; const value = item[key as keyof any];
// 处理特殊字符 // 处理特殊字符
if (typeof value === 'string') { if (typeof value === 'string') {
// 转义双引号,将"替换为"" // 转义双引号,将"替换为""
const escapedValue = value.replace(/"/g, '""'); const escapedValue = value.replace(/"/g, '""');
// 如果包含逗号或换行符,需要用双引号包裹 // 如果包含逗号或换行符,需要用双引号包裹
if (escapedValue.includes(',') || escapedValue.includes('\n')) { if (escapedValue.includes(',') || escapedValue.includes('\n')) {
return `"${escapedValue}"`; return `"${escapedValue}"`;
}
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); // 处理日期类型
// 写入文件 if (value instanceof Date) {
fs.writeFileSync(filePath, csvContent, 'utf8'); return value.toISOString();
return filePath;
}
// 根据类型返回不同结果
if (type === 'buffer') {
return Buffer.from(csvContent, 'utf8');
}
return csvContent;
} catch (error) {
throw new Error(`导出CSV文件失败: ${error.message}`);
}
}
/**
*
* @param str
* @returns
*/
removeLastParenthesesContent(str: string): string {
if (!str || typeof str !== 'string') {
return str;
}
// 辅助函数:删除指定位置的括号对及其内容
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;
}
} }
} // 处理undefined和null
if (value === undefined || value === null) {
if (rightIndex !== -1) { return '';
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;
} }
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);
if (lastLeftParenIndex !== -1) {
result = removeParenthesesAt(result, lastLeftParenIndex); // 写入文件
} fs.writeFileSync(filePath, csvContent, 'utf8');
console.log(`数据已成功导出至 ${filePath}`);
return filePath;
} }
// 2. 处理整个字符串的最后一个括号对 // 根据类型返回不同结果
let lastLeftParenIndex = result.lastIndexOf('('); if (type === 'buffer') {
if (lastLeftParenIndex !== -1) { return Buffer.from(csvContent, 'utf8');
result = removeParenthesesAt(result, lastLeftParenIndex);
} }
return result; return csvContent;
} catch (error) {
console.error('导出CSV时出错:', error);
throw new Error(`导出CSV文件失败: ${error.message}`);
} }
} }
}

File diff suppressed because it is too large Load Diff

View File

@ -8,16 +8,16 @@ import * as FormData from 'form-data';
import { SiteService } from './site.service'; import { SiteService } from './site.service';
import { Site } from '../entity/site.entity'; import { Site } from '../entity/site.entity';
import { UnifiedReviewDTO } from '../dto/site-api.dto'; import { UnifiedReviewDTO } from '../dto/site-api.dto';
import { ShopyyGetOneOrderResult, ShopyyReview } from '../dto/shopyy.dto'; import { ShopyyReview } from '../dto/shopyy.dto';
import { BatchOperationDTO, BatchOperationResultDTO } from '../dto/batch.dto'; import { BatchOperationDTO, BatchOperationResultDTO } from '../dto/batch.dto';
import { UnifiedSearchParamsDTO, ShopyyGetAllOrdersParams } from '../dto/api.dto'; import { UnifiedSearchParamsDTO } from '../dto/api.dto';
/** /**
* ShopYY平台服务实现 * ShopYY平台服务实现
*/ */
@Provide() @Provide()
export class ShopyyService { export class ShopyyService {
@Inject() @Inject()
logger: ILogger; logger:ILogger;
/** /**
* ShopYY评论列表 * ShopYY评论列表
* @param site * @param site
@ -128,7 +128,7 @@ export class ShopyyService {
* @returns URL * @returns URL
*/ */
private buildURL(baseUrl: string, endpoint: string): string { private buildURL(baseUrl: string, endpoint: string): string {
// ShopYY API URL格式:https://{shop}.shopyy.com/openapi/{version}/{endpoint} // ShopYY API URL格式https://{shop}.shopyy.com/openapi/{version}/{endpoint}
const base = baseUrl.replace(/\/$/, ''); const base = baseUrl.replace(/\/$/, '');
const end = endpoint.replace(/^\//, ''); const end = endpoint.replace(/^\//, '');
return `${base}/${end}`; return `${base}/${end}`;
@ -184,9 +184,9 @@ export class ShopyyService {
*/ */
public async fetchResourcePaged<T>(site: any, endpoint: string, params: Record<string, any> = {}) { public async fetchResourcePaged<T>(site: any, endpoint: string, params: Record<string, any> = {}) {
const response = await this.request(site, endpoint, 'GET', null, params); const response = await this.request(site, endpoint, 'GET', null, params);
return this.mapPageResponse<T>(response, params); return this.mapPageResponse<T>(response,params);
} }
mapPageResponse<T>(response: any, query: Record<string, any>) { mapPageResponse<T>(response:any,query: Record<string, any>){
if (response?.code !== 0) { if (response?.code !== 0) {
throw new Error(response?.msg) throw new Error(response?.msg)
} }
@ -272,7 +272,7 @@ export class ShopyyService {
const response = await this.request(site, `products/${productId}/variations/${variationId}`, 'GET'); const response = await this.request(site, `products/${productId}/variations/${variationId}`, 'GET');
return response.data; return response.data;
} }
mapOrderSearchParams(params: UnifiedSearchParamsDTO) { mapOrderSearchParams(params: UnifiedSearchParamsDTO){
const { after, before, ...restParams } = params; const { after, before, ...restParams } = params;
return { return {
...restParams, ...restParams,
@ -288,7 +288,7 @@ export class ShopyyService {
* @param pageSize * @param pageSize
* @returns * @returns
*/ */
async getOrders(site: any | number, page: number = 1, pageSize: number = 3000, params: ShopyyGetAllOrdersParams = {}): Promise<any> { async getOrders(site: any | number, page: number = 1, pageSize: number = 100, params: UnifiedSearchParamsDTO = {}): Promise<any> {
// 如果传入的是站点ID则获取站点配置 // 如果传入的是站点ID则获取站点配置
const siteConfig = typeof site === 'number' ? await this.siteService.get(site) : site; const siteConfig = typeof site === 'number' ? await this.siteService.get(site) : site;
@ -308,11 +308,12 @@ export class ShopyyService {
}; };
} }
async getAllOrders(site: any | number, params: ShopyyGetAllOrdersParams = {}, maxPages: number = 10, concurrencyLimit: number = 100): Promise<any> { 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, params); const firstPage = await this.getOrders(site, 1, 100);
const { items: firstPageItems, totalPages } = firstPage; const { items: firstPageItems, totalPages} = firstPage;
// const { page = 1, per_page = 100 } = params;
// 如果只有一页数据,直接返回 // 如果只有一页数据,直接返回
if (totalPages <= 1) { if (totalPages <= 1) {
return firstPageItems; return firstPageItems;
@ -320,7 +321,7 @@ export class ShopyyService {
// 限制最大页数,避免过多的并发请求 // 限制最大页数,避免过多的并发请求
const actualMaxPages = Math.min(totalPages, maxPages); const actualMaxPages = Math.min(totalPages, maxPages);
// 收集所有页面数据,从第二页开始 // 收集所有页面数据,从第二页开始
const allItems = [...firstPageItems]; const allItems = [...firstPageItems];
let currentPage = 2; let currentPage = 2;
@ -329,35 +330,35 @@ export class ShopyyService {
while (currentPage <= actualMaxPages) { while (currentPage <= actualMaxPages) {
const batchPromises: Promise<any[]>[] = []; const batchPromises: Promise<any[]>[] = [];
const batchSize = Math.min(concurrencyLimit, actualMaxPages - currentPage + 1); const batchSize = Math.min(concurrencyLimit, actualMaxPages - currentPage + 1);
// 创建当前批次的并发请求 // 创建当前批次的并发请求
for (let i = 0; i < batchSize; i++) { for (let i = 0; i < batchSize; i++) {
const page = currentPage + i; const page = currentPage + i;
const pagePromise = this.getOrders(site, page, 100, params) const pagePromise = this.getOrders(site, page, 100)
.then(pageResult => pageResult.items) .then(pageResult => pageResult.items)
.catch(error => { .catch(error => {
console.error(`获取第 ${page} 页数据失败:`, error); console.error(`获取第 ${page} 页数据失败:`, error);
return []; // 如果某页获取失败,返回空数组,不影响整体结果 return []; // 如果某页获取失败,返回空数组,不影响整体结果
}); });
batchPromises.push(pagePromise); batchPromises.push(pagePromise);
} }
// 等待当前批次完成 // 等待当前批次完成
const batchResults = await Promise.all(batchPromises); const batchResults = await Promise.all(batchPromises);
// 合并当前批次的数据 // 合并当前批次的数据
for (const pageItems of batchResults) { for (const pageItems of batchResults) {
allItems.push(...pageItems); allItems.push(...pageItems);
} }
// 移动到下一批次 // 移动到下一批次
currentPage += batchSize; currentPage += batchSize;
} }
return allItems; return allItems;
} }
/** /**
* ShopYY订单详情 * ShopYY订单详情
@ -365,7 +366,7 @@ export class ShopyyService {
* @param orderId ID * @param orderId ID
* @returns * @returns
*/ */
async getOrder(siteId: string, orderId: string): Promise<ShopyyGetOneOrderResult> { async getOrder(siteId: string, orderId: string): Promise<any> {
const site = await this.siteService.get(Number(siteId)); const site = await this.siteService.get(Number(siteId));
// ShopYY API: GET /orders/{id} // ShopYY API: GET /orders/{id}
@ -475,16 +476,13 @@ export class ShopyyService {
async createFulfillment(site: Site, orderId: string, data: any): Promise<any> { async createFulfillment(site: Site, orderId: string, data: any): Promise<any> {
// ShopYY API: POST /orders/{id}/shipments // ShopYY API: POST /orders/{id}/shipments
const fulfillmentData = { const fulfillmentData = {
data: [{ tracking_number: data.tracking_number,
order_number: orderId, carrier_code: data.carrier_code,
tracking_company: data.tracking_company, carrier_name: data.carrier_name,
tracking_number: data.tracking_number, shipping_method: data.shipping_method
carrier_code: data.carrier_code,
note: "note",
mode: ""
}]
}; };
const response = await this.request(site, `orders/fulfillments`, 'POST', fulfillmentData);
const response = await this.request(site, `orders/${orderId}/shipments`, 'POST', fulfillmentData);
return response.data; return response.data;
} }
@ -497,7 +495,7 @@ export class ShopyyService {
*/ */
async deleteFulfillment(site: any, orderId: string, fulfillmentId: string): Promise<boolean> { async deleteFulfillment(site: any, orderId: string, fulfillmentId: string): Promise<boolean> {
try { try {
// ShopYY API: DELETE /orders/fulfillments/{fulfillment_id} // ShopYY API: DELETE /orders/{order_id}/shipments/{fulfillment_id}
await this.request(site, `orders/${orderId}/fulfillments/${fulfillmentId}`, 'DELETE'); await this.request(site, `orders/${orderId}/fulfillments/${fulfillmentId}`, 'DELETE');
return true; return true;
} catch (error) { } catch (error) {
@ -545,7 +543,7 @@ export class ShopyyService {
try { try {
// ShopYY API: PUT /orders/{order_id}/shipments/{tracking_id} // ShopYY API: PUT /orders/{order_id}/shipments/{tracking_id}
const fulfillmentData: any = {}; const fulfillmentData: any = {};
// 只传递有值的字段 // 只传递有值的字段
if (data.tracking_number !== undefined) { if (data.tracking_number !== undefined) {
fulfillmentData.tracking_number = data.tracking_number; fulfillmentData.tracking_number = data.tracking_number;
@ -648,10 +646,10 @@ export class ShopyyService {
// ShopYY API: POST /products/batch // ShopYY API: POST /products/batch
const response = await this.request(site, 'products/batch', 'POST', data); const response = await this.request(site, 'products/batch', 'POST', data);
const result = response.data; const result = response.data;
// 转换 ShopYY 批量操作结果为统一格式 // 转换 ShopYY 批量操作结果为统一格式
const errors: Array<{ identifier: string, error: string }> = []; const errors: Array<{identifier: string, error: string}> = [];
// 假设 ShopYY 返回格式与 WooCommerce 类似: { create: [...], update: [...], delete: [...] } // 假设 ShopYY 返回格式与 WooCommerce 类似: { create: [...], update: [...], delete: [...] }
// 错误信息可能在每个项目的 error 字段中 // 错误信息可能在每个项目的 error 字段中
const checkForErrors = (items: any[]) => { const checkForErrors = (items: any[]) => {
@ -664,12 +662,12 @@ export class ShopyyService {
} }
}); });
}; };
// 检查每个操作类型的结果中的错误 // 检查每个操作类型的结果中的错误
if (result.create) checkForErrors(result.create); if (result.create) checkForErrors(result.create);
if (result.update) checkForErrors(result.update); if (result.update) checkForErrors(result.update);
if (result.delete) checkForErrors(result.delete); if (result.delete) checkForErrors(result.delete);
return { return {
total: (data.create?.length || 0) + (data.update?.length || 0) + (data.delete?.length || 0), 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), processed: (result.create?.length || 0) + (result.update?.length || 0) + (result.delete?.length || 0),

View File

@ -7,7 +7,6 @@ import { SiteService } from './site.service';
import { WPService } from './wp.service'; import { WPService } from './wp.service';
import { ProductService } from './product.service'; import { ProductService } from './product.service';
import { UnifiedProductDTO } from '../dto/site-api.dto'; import { UnifiedProductDTO } from '../dto/site-api.dto';
import { Product } from '../entity/product.entity';
@Provide() @Provide()
export class SiteApiService { export class SiteApiService {
@ -53,7 +52,7 @@ export class SiteApiService {
* @param siteProduct * @param siteProduct
* @returns ERP产品信息的站点商品 * @returns ERP产品信息的站点商品
*/ */
async enrichSiteProductWithErpInfo(siteId: number, siteProduct: UnifiedProductDTO): Promise<UnifiedProductDTO & { erpProduct?: Product }> { async enrichSiteProductWithErpInfo(siteId: number, siteProduct: any): Promise<any> {
if (!siteProduct || !siteProduct.sku) { if (!siteProduct || !siteProduct.sku) {
return siteProduct; return siteProduct;
} }
@ -65,7 +64,18 @@ export class SiteApiService {
// 将ERP产品信息合并到站点商品中 // 将ERP产品信息合并到站点商品中
return { return {
...siteProduct, ...siteProduct,
erpProduct, 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产品字段
}
}; };
} catch (error) { } catch (error) {
// 如果找不到对应的ERP产品返回原始站点商品 // 如果找不到对应的ERP产品返回原始站点商品
@ -80,7 +90,7 @@ export class SiteApiService {
* @param siteProducts * @param siteProducts
* @returns ERP产品信息的站点商品列表 * @returns ERP产品信息的站点商品列表
*/ */
async enrichSiteProductsWithErpInfo(siteId: number, siteProducts: UnifiedProductDTO[]): Promise<(UnifiedProductDTO & { erpProduct?: Product })[]> { async enrichSiteProductsWithErpInfo(siteId: number, siteProducts: any[]): Promise<any[]> {
if (!siteProducts || !siteProducts.length) { if (!siteProducts || !siteProducts.length) {
return siteProducts; return siteProducts;
} }
@ -142,7 +152,7 @@ export class SiteApiService {
const result = await this.upsertProduct(siteId, product); const result = await this.upsertProduct(siteId, product);
// 判断是创建还是更新 // 判断是创建还是更新
if (result && result.id) { if (result && result.id) {
// 简单判断:如果产品原本没有ID而现在有了说明是创建的 // 简单判断如果产品原本没有ID而现在有了说明是创建的
if (!product.id || !product.id.toString().trim()) { if (!product.id || !product.id.toString().trim()) {
results.created.push(result); results.created.push(result);
} else { } else {

View File

@ -15,19 +15,8 @@ export class StatisticsService {
orderItemRepository: Repository<OrderItem>; orderItemRepository: Repository<OrderItem>;
async getOrderStatistics(params: OrderStatisticsParams) { async getOrderStatistics(params: OrderStatisticsParams) {
const { startDate, endDate, grouping, siteId, country } = params; const { startDate, endDate, grouping, siteId } = params;
// const keywords = keyword ? keyword.split(' ').filter(Boolean) : []; // 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 start = dayjs(startDate).format('YYYY-MM-DD');
const end = dayjs(endDate).add(1, 'd').format('YYYY-MM-DD'); const end = dayjs(endDate).add(1, 'd').format('YYYY-MM-DD');
let sql let sql
@ -65,24 +54,22 @@ export class StatisticsService {
AND o.status IN('processing','completed') AND o.status IN('processing','completed')
`; `;
if (siteId) sql += ` AND o.siteId=${siteId}`; if (siteId) sql += ` AND o.siteId=${siteId}`;
if (siteIds.length) sql += ` AND o.siteId IN (${siteIds.join(',')})`;
sql += ` sql += `
GROUP BY o.id, o.date_paid, o.customer_email, o.total, o.source_type, o.siteId, o.utm_source GROUP BY o.id, o.date_paid, o.customer_email, o.total, o.source_type, o.siteId, o.utm_source
), ),
order_sales_summary AS ( order_sales_summary AS (
SELECT SELECT
orderId, orderId,
SUM(CASE WHEN brand = 'zyn' THEN quantity ELSE 0 END) AS zyn_quantity, SUM(CASE WHEN name LIKE '%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 name LIKE '%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 name LIKE '%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 name LIKE '%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 name LIKE '%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 name LIKE '%yoone%' AND name LIKE '%3%' 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 name LIKE '%yoone%' AND name LIKE '%6%' 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 name LIKE '%yoone%' AND name LIKE '%9%' 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 name LIKE '%yoone%' AND name LIKE '%12%' 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 SUM(CASE WHEN name LIKE '%yoone%' AND name LIKE '%15%' THEN quantity ELSE 0 END) AS yoone_15_quantity
FROM order_sale FROM order_sale
GROUP BY orderId GROUP BY orderId
), ),
@ -260,25 +247,22 @@ export class StatisticsService {
LEFT JOIN order_item oi ON o.id = oi.orderId LEFT JOIN order_item oi ON o.id = oi.orderId
WHERE o.date_paid IS NOT NULL 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}'
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 GROUP BY o.id, o.date_paid, o.customer_email, o.total, o.source_type, o.siteId, o.utm_source
), ),
order_sales_summary AS ( order_sales_summary AS (
SELECT SELECT
orderId, orderId,
SUM(CASE WHEN brand = 'zyn' THEN quantity ELSE 0 END) AS zyn_quantity, SUM(CASE WHEN name LIKE '%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 name LIKE '%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 name LIKE '%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 name LIKE '%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 name LIKE '%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 name LIKE '%yoone%' AND name LIKE '%3%' 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 name LIKE '%yoone%' AND name LIKE '%6%' 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 name LIKE '%yoone%' AND name LIKE '%9%' 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 name LIKE '%yoone%' AND name LIKE '%12%' 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 SUM(CASE WHEN name LIKE '%yoone%' AND name LIKE '%15%' THEN quantity ELSE 0 END) AS yoone_15_quantity
FROM order_sale FROM order_sale
GROUP BY orderId GROUP BY orderId
), ),
@ -455,27 +439,23 @@ export class StatisticsService {
LEFT JOIN first_order f ON o.customer_email = f.customer_email LEFT JOIN first_order f ON o.customer_email = f.customer_email
LEFT JOIN order_item oi ON o.id = oi.orderId LEFT JOIN order_item oi ON o.id = oi.orderId
WHERE o.date_paid IS NOT NULL 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') 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 GROUP BY o.id, o.date_paid, o.customer_email, o.total, o.source_type, o.siteId, o.utm_source
), ),
order_sales_summary AS ( order_sales_summary AS (
SELECT SELECT
orderId, orderId,
SUM(CASE WHEN brand = 'zyn' THEN quantity ELSE 0 END) AS zyn_quantity, SUM(CASE WHEN name LIKE '%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 name LIKE '%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 name LIKE '%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 name LIKE '%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 name LIKE '%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 name LIKE '%yoone%' AND name LIKE '%3%' 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 name LIKE '%yoone%' AND name LIKE '%6%' 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 name LIKE '%yoone%' AND name LIKE '%9%' 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 name LIKE '%yoone%' AND name LIKE '%12%' 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 SUM(CASE WHEN name LIKE '%yoone%' AND name LIKE '%15%' THEN quantity ELSE 0 END) AS yoone_15_quantity
FROM order_sale FROM order_sale
GROUP BY orderId GROUP BY orderId
), ),
@ -1334,14 +1314,7 @@ export class StatisticsService {
} }
async getOrderSorce(params) { async getOrderSorce(params) {
const { country } = params; const sql = `
let siteIds = []
if (country) {
siteIds = await this.getSiteIds(country)
}
let sql = `
WITH cutoff_months AS ( WITH cutoff_months AS (
SELECT SELECT
DATE_FORMAT(DATE_SUB(CURDATE(), INTERVAL 7 MONTH), '%Y-%m') AS start_month, DATE_FORMAT(DATE_SUB(CURDATE(), INTERVAL 7 MONTH), '%Y-%m') AS start_month,
@ -1353,10 +1326,7 @@ export class StatisticsService {
DATE_FORMAT(MIN(date_paid), '%Y-%m') AS first_order_month, DATE_FORMAT(MIN(date_paid), '%Y-%m') AS first_order_month,
SUM(total) AS first_order_total SUM(total) AS first_order_total
FROM \`order\` 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 GROUP BY customer_email
), ),
order_months AS ( order_months AS (
@ -1364,10 +1334,7 @@ export class StatisticsService {
customer_email, customer_email,
DATE_FORMAT(date_paid, '%Y-%m') AS order_month DATE_FORMAT(date_paid, '%Y-%m') AS order_month
FROM \`order\` 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 ( filtered_orders AS (
SELECT o.customer_email, o.order_month, u.first_order_month,u.first_order_total, c.start_month SELECT o.customer_email, o.order_month, u.first_order_month,u.first_order_total, c.start_month
@ -1399,7 +1366,7 @@ export class StatisticsService {
ORDER BY order_month DESC, first_order_month_group ORDER BY order_month DESC, first_order_month_group
` `
let inactiveSql = ` const inactiveSql = `
WITH WITH
cutoff_months AS ( cutoff_months AS (
SELECT SELECT
@ -1414,10 +1381,7 @@ export class StatisticsService {
date_paid, date_paid,
total total
FROM \`order\` 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 ( filtered_users AS (
@ -1560,13 +1524,4 @@ 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)
}
} }

View File

@ -1,8 +1,6 @@
/** /**
* wp
* https://developer.wordpress.org/rest-api/reference/media/
* woocommerce
* *
* https://developer.wordpress.org/rest-api/reference/media/
*/ */
import { Inject, Provide } from '@midwayjs/core'; import { Inject, Provide } from '@midwayjs/core';
import axios, { AxiosRequestConfig } from 'axios'; import axios, { AxiosRequestConfig } from 'axios';
@ -12,7 +10,7 @@ import { IPlatformService } from '../interface/platform.interface';
import { BatchOperationDTO, BatchOperationResultDTO } from '../dto/batch.dto'; import { BatchOperationDTO, BatchOperationResultDTO } from '../dto/batch.dto';
import * as FormData from 'form-data'; import * as FormData from 'form-data';
import * as fs from 'fs'; import * as fs from 'fs';
import { WooProduct, WooVariation, WpMediaGetListParams } from '../dto/woocommerce.dto'; import { WooProduct, WooVariation } from '../dto/woocommerce.dto';
const MAX_PAGE_SIZE = 100; const MAX_PAGE_SIZE = 100;
@Provide() @Provide()
export class WPService implements IPlatformService { export class WPService implements IPlatformService {
@ -256,7 +254,7 @@ export class WPService implements IPlatformService {
} }
// 导出 WooCommerce 产品为特殊CSV(平台特性) // 导出 WooCommerce 产品为特殊CSV(平台特性)
async exportProductsCsvSpecial(site: any, page: number = 1, pageSize: number = 100): Promise<string> { async exportProductsCsvSpecial(site: any, page: number = 1, pageSize: number = 100): Promise<string> {
const list = await this.getProducts(site, { page, per_page: pageSize }); const list = await this.getProducts(site, { page, per_page: pageSize });
const header = ['id','name','type','status','sku','regular_price','sale_price','stock_status','stock_quantity']; const header = ['id','name','type','status','sku','regular_price','sale_price','stock_status','stock_quantity'];
@ -1046,7 +1044,20 @@ export class WPService implements IPlatformService {
}; };
} }
public async fetchMediaPaged(site: any, params: Partial<WpMediaGetListParams> = {}) { 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';
}
}
const apiUrl = site.apiUrl; const apiUrl = site.apiUrl;
const { consumerKey, consumerSecret } = site as any; const { consumerKey, consumerSecret } = site as any;
const endpoint = 'wp/v2/media'; const endpoint = 'wp/v2/media';
@ -1055,21 +1066,17 @@ public async fetchMediaPaged(site: any, params: Partial<WpMediaGetListParams> =
const response = await axios.get(url, { const response = await axios.get(url, {
headers: { Authorization: `Basic ${auth}` }, headers: { Authorization: `Basic ${auth}` },
params: { params: {
...params, ...where,
page: params.page ?? 1, ...(params.search ? { search: params.search } : {}),
per_page: params.per_page ?? 20, ...(orderby ? { orderby } : {}),
...(order ? { order } : {}),
page,
per_page
} }
}); });
// 检查是否有错误信息
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 total = Number(response.headers['x-wp-total'] || 0);
const totalPages = Number(response.headers['x-wp-totalpages'] || 0); const totalPages = Number(response.headers['x-wp-totalpages'] || 0);
return { items: response.data, total, totalPages, page:params.page ?? 1, per_page: params.per_page ?? 20, page_size: params.per_page ?? 20 }; return { items: response.data, total, totalPages, page, per_page, page_size: per_page };
} }
/** /**
* *

View File

@ -1,11 +0,0 @@
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;
};

View File

@ -1,26 +0,0 @@
// 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();

105
test-site-sku-methods.md Normal file
View File

@ -0,0 +1,105 @@
# 产品站点SKU查询方法测试文档
## 新增和更新的API接口
### 1. 根据产品ID获取站点SKU列表
**接口**: `GET /product/:id/site-skus`
**功能**: 获取指定产品的所有站点SKU列表
**返回**: 站点SKU对象数组按创建时间升序排列
### 2. 根据站点SKU查询产品
**接口**: `GET /product/site-sku/:siteSku`
**功能**: 根据站点SKU代码查询对应的产品信息
**返回**: 完整的产品对象包含站点SKU、分类、属性等关联数据
### 3. 根据产品ID获取产品详情
**接口**: `GET /product/:id`
**功能**: 获取产品的完整详情信息
**返回**: 完整的产品对象包含站点SKU、分类、属性、组成等关联数据
### 4. 现有接口的增强
#### 4.1 根据SKU查询产品
**接口**: `GET /product/sku/:sku`
**增强**: 现在返回的产品信息包含关联的站点SKU数据
#### 4.2 搜索产品
**接口**: `GET /product/search?name=:name`
**增强**: 搜索结果现在包含每个产品的站点SKU数据
#### 4.3 获取产品列表
**接口**: `GET /product/list`
**增强**: 产品列表中的每个产品现在都包含站点SKU数据
## 服务层新增方法
### ProductService新增方法
1. **getProductSiteSkus(productId: number)**: Promise<ProductSiteSku[]>
- 获取指定产品的所有站点SKU
- 包含产品关联信息
- 按创建时间排序
2. **getProductById(id: number)**: Promise<Product>
- 根据产品ID获取完整产品信息
- 包含站点SKU、分类、属性、组成等所有关联数据
- 自动处理单品和混装商品的组成信息
3. **findProductBySiteSku(siteSku: string)**: Promise<Product>
- 根据站点SKU查询对应的产品
- 返回完整的产品信息
- 如果站点SKU不存在则抛出错误
### 现有方法增强
1. **findProductsByName(name: string)**: 现在包含站点SKU数据
2. **findProductBySku(sku: string)**: 现在包含站点SKU数据
3. **getProductList**: 已经包含站点SKU数据无需更改
## 使用示例
### 获取产品的站点SKU列表
```javascript
// GET /product/123/site-skus
// 返回:
[
{
"id": 1,
"siteSku": "SITE-SKU-001",
"productId": 123,
"createdAt": "2024-01-01T00:00:00Z",
"updatedAt": "2024-01-01T00:00:00Z"
}
]
```
### 根据站点SKU查询产品
```javascript
// GET /product/site-sku/SITE-SKU-001
// 返回完整的产品对象,包含:
// - 基本信息SKU、名称、价格等
// - 分类信息
// - 属性信息
// - 站点SKU列表
// - 组成信息
```
### 获取产品详情
```javascript
// GET /product/123
// 返回完整的产品对象与站点SKU查询类似
```
## 数据库查询优化
所有新增和更新的方法都使用了TypeORM的关联查询确保
- 一次查询获取所有需要的数据
- 避免N+1查询问题
- 包含必要的关联关系分类、属性、站点SKU、组成
## 错误处理
所有方法都包含适当的错误处理:
- 产品不存在时抛出明确的错误信息
- 站点SKU不存在时抛出明确的错误信息
- 控制器层统一处理错误并返回适当的HTTP响应