zksu
/
API
forked from yoone/API
1
0
Fork 0

Compare commits

...

54 Commits

Author SHA1 Message Date
tikkhun 3968fd8965 fix(woocommerce.adapter): 修正物流追踪日期格式转换问题 2026-01-23 18:36:03 +08:00
tikkhun c26918d4db feat(dto): 在 FulfillmentDTO 中添加物流产品代码字段 2026-01-23 18:34:37 +08:00
tikkhun 16d27179e7 feat(woocommerce): 重构订单物流追踪信息处理方式
使用元数据中的物流追踪信息替代原有接口
移除冗余的履行信息获取逻辑
2026-01-23 18:29:20 +08:00
tikkhun a556ab69bf feat(订单服务): 在订单统计查询中添加sku字段
在订单商品统计查询SQL中增加sku字段,以便按商品SKU进行分组统计和展示
2026-01-23 17:46:04 +08:00
tikkhun efbd318917 docs(order.entity): 更新total字段的ApiProperty描述 2026-01-23 17:36:02 +08:00
zhuotianyuan e8424afd91 refactor(订单服务): 替换console.log为logger并移除重复日志
移除重复的console.log调用,统一使用logger进行日志记录
清理调试日志并优化日志级别使用
2026-01-23 17:02:10 +08:00
zhuotianyuan 5f16801f98 refactor: 移除调试用的console.log语句 2026-01-23 16:33:14 +08:00
zhuotianyuan 22a13ce0b8 feat(logistics): 完善freightwaves运费试算接口实现
重构convertToFreightwavesRateTry方法,添加地址查询逻辑
移除freightwaves服务中无用的测试方法
修复订单同步日志格式和注释
2026-01-23 16:29:10 +08:00
zhuotianyuan 2f57dc0d8c fix(logistics): 修复shopyy订单发货逻辑并优化freightwaves集成
修复shopyy平台订单发货时fulfillment接口调用问题,调整请求数据结构
优化freightwaves服务集成,添加订单查询功能
移除冗余代码,清理注释
2026-01-22 19:36:02 +08:00
zhuotianyuan 926e531d4f refactor(dto): 移除重复的ShopyyGetAllOrdersParams类和冗余字段
删除api.dto.ts中重复定义的ShopyyGetAllOrdersParams类
移除shopyy.dto.ts中冗余的date_paid字段
2026-01-22 17:17:46 +08:00
zhuotianyuan 66a70f6209 fix(config): 更新本地数据库配置为远程服务器地址
将数据库连接配置从本地localhost改为远程服务器地址13.212.62.127,并调整端口为3306
同时启用数据库日志记录功能
2026-01-22 17:12:17 +08:00
zhuotianyuan 1eacee307d refactor(订单服务): 优化产品详情查询逻辑
使用 productService 的 getComponentDetailFromSiteSku 方法替代直接查询,简化代码并增加数量检查
2026-01-22 17:06:51 +08:00
zhuotianyuan bff03de8b0 feat(logistics): 添加freightwaves物流平台支持
新增freightwaves物流平台集成,包括运单创建、状态查询和费用试算功能
添加sync_tms.job定时任务用于同步freightwaves运单状态
扩展ShipmentBookDTO和ShipmentFeeBookDTO以支持多物流平台
重构物流服务以支持uniuni和freightwaves双平台
2026-01-22 16:58:19 +08:00
zhuotianyuan 86aa5f5790 fix: 修复测试方法调用和订单内容格式
修复测试文件中错误的方法调用,从testQueryOrder改为testCreateOrder
调整订单内容格式,移除SKU显示并简化格式
修正电话号码字段的类型断言问题
修复日期格式错误,从mm改为MM
更新API基础URL和端点路径
移除不必要的日志对象调用,改用console.log
2026-01-22 16:57:29 +08:00
zhuotianyuan 52fa7d651e feat: 添加产品图片URL字段并优化订单处理逻辑
添加产品实体中的图片URL字段
更新订单服务以支持更多查询参数和分页
修改数据库连接配置为生产环境
调整运费服务API基础URL
优化订单适配器中的字段映射逻辑
2026-01-22 16:57:29 +08:00
zhuotianyuan 0ea834218d fix(shopyy): 修复订单查询参数映射问题并添加时间范围支持
修正shopyy服务中获取所有订单的参数映射逻辑,添加支付时间范围支持
统一处理日期格式转换,确保参数正确传递
同时清理合并冲突标记和冗余代码
2026-01-22 16:55:53 +08:00
zhuotianyuan 86fd31ac12 feat(webhook): 添加对shoppy平台webhook的支持
- 在site.entity.ts中添加webhookUrl字段
- 在auth.middleware.ts中添加/shoppy路由到白名单
- 在webhook.controller.ts中实现shoppy平台webhook处理逻辑

fix(webhook): 更新webhook控制器中的密钥值

refactor(entity): 将可选字段明确标记为可选类型

feat(adapter): 公开映射方法以支持统一接口调用

将各适配器中的私有映射方法改为公开,并在接口中定义统一方法签名
修改webhook控制器以使用适配器映射方法处理订单数据

feat: 添加订单支付日期字段并支持国家筛选

- 在ShopyyOrder接口中添加date_paid字段
- 在OrderStatisticsParams中添加country数组字段用于国家筛选
- 修改统计服务以支持按国家筛选订单数据
- 更新数据库配置和同步设置
- 优化订单服务中的类型定义和查询条件

refactor(webhook): 移除未使用的shoppy webhook处理逻辑

fix(订单服务): 修复订单内容括号处理并添加同步日志

添加订单同步过程的调试日志
修复订单内容中括号内容的处理逻辑
修正控制器方法名拼写错误
2026-01-22 16:52:48 +08:00
tikkhun 75056db42c refactor(woocommerce): 优化参数处理逻辑并提取工具函数
将参数转换逻辑提取到独立的工具模块中
合并重复的include参数处理逻辑
添加对parent_exclude参数的支持
2026-01-22 15:05:41 +08:00
tikkhun d5384944a4 feat(woocommerce): 添加物流追踪创建和更新接口
添加 WooFulfillmentCreateParams 接口定义并重构物流追踪创建和更新方法
使用统一的 FulfillmentDTO 类型简化参数处理
2026-01-22 14:44:42 +08:00
tikkhun cb876e8c0f refactor(物流): 更新物流相关接口和DTO以支持可选字段
重构物流追踪相关接口和DTO,将order_item_id和quantity改为可选字段
添加tracking_id字段到FulfillmentDTO
优化woocommerce物流数据结构映射
更新package-lock.json添加faker依赖
2026-01-22 11:38:44 +08:00
tikkhun 71b2c249be feat(产品服务): 添加产品分组查询功能并优化相关服务
新增产品分组查询接口,支持按指定字段或属性对产品进行分组
优化产品服务中的查询逻辑,修复属性关联查询的字段错误
完善媒体服务接口参数类型定义,增强类型安全性
重构ERP产品信息合并逻辑,使用实体类型替代手动映射
2026-01-21 10:43:14 +08:00
tikkhun b3b7ee4793 refactor(订单服务): 重构订单组件详情获取逻辑
将订单服务中的产品详情查询逻辑提取到产品服务中
处理混合SKU和bundle产品的特殊情况
2026-01-17 16:58:09 +08:00
tikkhun b7101ac866 refactor(service): 重构订单服务中的品牌属性检查逻辑
将硬编码的品牌检查逻辑改为直接存储品牌和其他属性值,提高代码的可维护性和扩展性
2026-01-17 16:58:09 +08:00
tikkhun 72cd20fcd6 fix(订单服务): 修正套餐类型判断逻辑并添加注释
将isPackage的判断从子产品改为父产品类型,与业务逻辑一致
添加externalOrderItemId的注释说明
2026-01-17 16:58:09 +08:00
tikkhun 8766cf4a4c feat(订单): 添加父产品ID字段用于统计套餐
在订单服务和订单销售实体中添加 parentProductId 字段,用于区分套餐产品和单品。如果是套餐产品则记录父产品ID,单品则不记录该字段
2026-01-17 16:58:09 +08:00
tikkhun d39341d683 feat(产品服务): 增加分类名称支持并优化产品查询逻辑
添加分类名称字段支持,允许通过分类名称创建和更新产品
移除重复的查询条件逻辑,简化产品服务查询方法
重构产品导入功能,改进CSV记录到产品对象的映射
2026-01-17 16:58:09 +08:00
tikkhun 7f04de4583 fix(product): 将sku精确匹配改为模糊查询
移除重复的sku过滤条件,统一使用LIKE进行模糊查询
2026-01-17 16:58:09 +08:00
tikkhun bdac4860df feat(产品): 添加产品属性过滤和分组功能
- 在 ProductWhereFilter 接口中添加 attributes 字段用于属性过滤
- 新增 getAllProducts 方法支持按品牌过滤产品
- 新增 getProductsGroupedByAttribute 方法实现按属性分组产品
- 在查询构建器中添加属性过滤逻辑
2026-01-17 16:58:09 +08:00
zhuotianyuan fff62d6864 feat: 添加产品图片URL字段并优化订单处理逻辑
添加产品实体中的图片URL字段
更新订单服务以支持更多查询参数和分页
修改数据库连接配置为生产环境
调整运费服务API基础URL
优化订单适配器中的字段映射逻辑

fix: 修正测试文件中错误的服务引用路径
2026-01-17 10:52:19 +08:00
zhuotianyuan c75c0a614f fix(shopyy): 修复订单查询参数映射问题并添加时间范围支持
修正shopyy服务中获取所有订单的参数映射逻辑,添加支付时间范围支持
统一处理日期格式转换,确保参数正确传递
同时清理合并冲突标记和冗余代码
2026-01-17 10:52:14 +08:00
zhuotianyuan bfa03fc6a0 feat(freightwaves): 添加TMS系统API集成和测试方法 2026-01-17 10:43:34 +08:00
zhuotianyuan 16539b133f style: 修复 typeorm 配置缩进问题 2026-01-17 10:43:34 +08:00
zhuotianyuan 9fc1bedb0c fix(config): 将数据库配置更改为本地开发环境
更新数据库连接配置为本地开发环境,包括主机、端口和密码
移除自动同步数据库的配置项
2026-01-17 10:43:34 +08:00
zhuotianyuan 0f79b7536a feat(webhook): 添加对shoppy平台webhook的支持
- 在site.entity.ts中添加webhookUrl字段
- 在auth.middleware.ts中添加/shoppy路由到白名单
- 在webhook.controller.ts中实现shoppy平台webhook处理逻辑

fix(webhook): 更新webhook控制器中的密钥值

refactor(entity): 将可选字段明确标记为可选类型

feat(adapter): 公开映射方法以支持统一接口调用

将各适配器中的私有映射方法改为公开,并在接口中定义统一方法签名
修改webhook控制器以使用适配器映射方法处理订单数据

feat: 添加订单支付日期字段并支持国家筛选

- 在ShopyyOrder接口中添加date_paid字段
- 在OrderStatisticsParams中添加country数组字段用于国家筛选
- 修改统计服务以支持按国家筛选订单数据
- 更新数据库配置和同步设置
- 优化订单服务中的类型定义和查询条件

refactor(webhook): 移除未使用的shoppy webhook处理逻辑

fix(订单服务): 修复订单内容括号处理并添加同步日志

添加订单同步过程的调试日志
修复订单内容中括号内容的处理逻辑
修正控制器方法名拼写错误
2026-01-17 10:43:31 +08:00
tikkhun fbbb86ae37 feat: 添加产品图片字段并优化字典导入功能
添加产品图片URL字段到产品相关实体和DTO
重命名字典导入方法并优化导入逻辑
新增站点商品实体和ShopYY商品更新接口
优化Excel处理以支持UTF-8编码
2026-01-14 19:16:30 +08:00
tikkhun 56deb447b3 feat(dto): 新增订单支付状态枚举类型
feat(controller): 重命名产品导入方法为importProductsFromTable

feat(service): 使用xlsx替换csv解析器处理产品导入

refactor(adapter): 完善订单数据结构定义和类型映射

docs(dto): 补充Shopyy订单和产品接口的详细注释
2026-01-13 16:26:30 +08:00
tikkhun 68574dbc7a refactor(order): 重构订单相关实体和服务逻辑
重构 OrderSale 实体,移除品牌判断标志字段,改为直接存储品牌、口味等属性
修改订单服务和统计服务,使用新的属性字段进行查询和统计
优化产品查询时的关联关系加载
2026-01-12 15:13:37 +08:00
tikkhun eb5cb215a9 fix: 修正shopyy适配器中email字段的拼写错误 2026-01-10 15:50:06 +08:00
tikkhun ca0b5e63a7 refactor(shopyy): 统一账单地址字段名并添加空值检查
将 billing_address 字段重命名为 bill_address 以保持命名一致性
在订单映射方法中添加空值检查防止空对象错误
2026-01-10 15:48:39 +08:00
tikkhun 5d7e0090aa style: 修复代码格式问题,包括空格和空行 2026-01-10 15:17:08 +08:00
tikkhun ecdedcc041 fix: 修复订单服务中产品属性和组件处理的问题
处理产品属性为空的情况,避免空指针异常
为产品组件查询添加关联关系
在订单销售记录创建时增加对空产品的过滤
添加新的品牌判断逻辑
2026-01-10 15:16:29 +08:00
tikkhun b2ee61e47d refactor: 移除未使用的导入和注释掉的生命周期钩子 2026-01-10 15:16:29 +08:00
tikkhun 64c1d1afe5 refactor(订单服务): 移除冗余的订单可编辑性检查注释
注释说明检查应在 save 方法中进行
2026-01-10 15:14:12 +08:00
tikkhun 4eb45af452 feat(订单): 增强订单相关功能及数据模型
- 在订单实体中添加orderItems和orderSales字段
- 优化QueryOrderSalesDTO,增加排序字段和更多描述信息
- 重构saveOrderSale方法,使用产品属性自动设置品牌和强度
- 在订单查询中返回关联的orderItems和orderSales数据
- 添加getAttributesObject方法处理产品属性
2026-01-10 15:14:12 +08:00
tikkhun a8d12a695e fix(product.service): 支持英文分号和逗号分隔siteSkus字段
修改siteSkus字段的分隔符处理逻辑,使其同时支持英文分号和逗号作为分隔符,提高数据兼容性
2026-01-10 15:09:52 +08:00
zhuotianyuan a00a95c9a3 style: 修复 typeorm 配置缩进问题 2026-01-10 07:07:24 +00:00
zhuotianyuan 82c8640f0c fix(config): 将数据库配置更改为本地开发环境
更新数据库连接配置为本地开发环境,包括主机、端口和密码
移除自动同步数据库的配置项
2026-01-10 07:07:24 +00:00
zhuotianyuan cb00076bd3 feat(webhook): 添加对shoppy平台webhook的支持
- 在site.entity.ts中添加webhookUrl字段
- 在auth.middleware.ts中添加/shoppy路由到白名单
- 在webhook.controller.ts中实现shoppy平台webhook处理逻辑

fix(webhook): 更新webhook控制器中的密钥值

refactor(entity): 将可选字段明确标记为可选类型

feat(adapter): 公开映射方法以支持统一接口调用

将各适配器中的私有映射方法改为公开,并在接口中定义统一方法签名
修改webhook控制器以使用适配器映射方法处理订单数据

feat: 添加订单支付日期字段并支持国家筛选

- 在ShopyyOrder接口中添加date_paid字段
- 在OrderStatisticsParams中添加country数组字段用于国家筛选
- 修改统计服务以支持按国家筛选订单数据
- 更新数据库配置和同步设置
- 优化订单服务中的类型定义和查询条件

refactor(webhook): 移除未使用的shoppy webhook处理逻辑

fix(订单服务): 修复订单内容括号处理并添加同步日志

添加订单同步过程的调试日志
修复订单内容中括号内容的处理逻辑
修正控制器方法名拼写错误
2026-01-10 07:07:24 +00:00
tikkhun cdff083940 fix(logistics): 处理获取运单状态失败的情况并优化批量更新
当获取运单状态返回FAIL时抛出错误信息
将运单状态更新改为并行处理并添加日志记录
2026-01-09 11:12:51 +08:00
tikkhun 3d6f488a70 docs: 删除过期的测试站点SKU方法文档 2026-01-08 21:04:25 +08:00
tikkhun a067720c76 style: 统一中文标点符号为英文格式并更新数据库配置
refactor: 修改注释和描述中的中文标点符号为英文格式
chore: 更新本地数据库配置的端口、密码和数据库名
docs: 删除迁移指南和排列组合修复文档
2026-01-08 21:02:57 +08:00
tikkhun 2888d62037 refactor(webhook): 重构webhook控制器逻辑,使用siteApiService适配器
- 移除对UnifiedOrderDTO的直接使用,改为通过适配器转换订单数据
- 统一处理woocommerce和shoppy的webhook验证逻辑
- 清理冗余代码和注释
2026-01-08 20:54:45 +08:00
tikkhun 3f5fb6adba feat: 重构站点API适配器接口并实现多平台支持
refactor(adapter): 重构适配器接口结构,分离不同实体的映射方法
feat(product): 增强产品同步功能,支持通过SKU查找和更新
fix(order): 修复订单同步和履约相关的问题
feat(template): 改进SKU模板逻辑,支持按分类属性排序
chore: 移除未使用的媒体控制器和相关代码
2026-01-08 20:40:12 +08:00
zhuotianyuan 983ba47dbf feat(adapter): 公开映射方法以支持统一接口调用
将各适配器中的私有映射方法改为公开,并在接口中定义统一方法签名
修改webhook控制器以使用适配器映射方法处理订单数据
2026-01-08 10:15:31 +00:00
58 changed files with 6018 additions and 3080 deletions

View File

@ -1,49 +0,0 @@
# 数据库迁移指南
为了支持区域坐标功能,需要执行数据库迁移操作,将新增的 `latitude``longitude` 字段添加到数据库表中.
## 执行迁移步骤
### 1. 生成迁移文件
运行以下命令生成迁移文件:
```bash
npm run migration:generate -- ./src/db/migrations/AddCoordinatesToArea
```
### 2. 检查迁移文件
生成的迁移文件会自动包含添加 `latitude``longitude` 字段的SQL语句.您可以检查文件内容确保迁移逻辑正确.
### 3. 执行迁移
运行以下命令执行迁移,将更改应用到数据库:
```bash
npm run migration:run
```
## 手动迁移SQL(可选)
如果需要手动执行SQL,可以使用以下语句:
```sql
ALTER TABLE `area`
ADD COLUMN `latitude` DECIMAL(10,6) NULL AFTER `name`,
ADD COLUMN `longitude` DECIMAL(10,6) NULL AFTER `latitude`;
```
## 回滚迁移(如需)
如果遇到问题,可以使用以下命令回滚迁移:
```bash
npm run typeorm -- migration:revert -d src/db/datasource.ts
```
## 注意事项
- 确保在执行迁移前备份数据库
- 迁移不会影响现有数据,新增字段默认为 NULL
- 迁移后,可以通过API开始为区域添加坐标信息

6
package-lock.json generated
View File

@ -524,9 +524,9 @@
}
},
"node_modules/@faker-js/faker": {
"version": "10.1.0",
"resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-10.1.0.tgz",
"integrity": "sha512-C3mrr3b5dRVlKPJdfrAXS8+dq+rq8Qm5SNRazca0JKgw1HQERFmrVb0towvMmw5uu8hHKNiQasMaR/tydf3Zsg==",
"version": "10.2.0",
"resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-10.2.0.tgz",
"integrity": "sha512-rTXwAsIxpCqzUnZvrxVh3L0QA0NzToqWBLAhV+zDV3MIIwiQhAZHMdPCIaj5n/yADu/tyk12wIPgL6YHGXJP+g==",
"funding": [
{
"type": "opencollective",

View File

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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

@ -79,6 +79,31 @@ export class ProductController {
}
}
@ApiOkResponse({
description: '成功返回分组后的产品列表',
schema: {
type: 'object',
additionalProperties: {
type: 'array',
items: {
$ref: '#/components/schemas/Product',
},
},
},
})
@Get('/list/grouped')
async getProductListGrouped(
@Query() query: UnifiedSearchParamsDTO<ProductWhereFilter>
): Promise<any> {
try {
const data = await this.productService.getProductListGrouped(query);
return successResponse(data);
} catch (error) {
this.logger.error('获取分组产品列表失败', error);
return errorResponse(error?.message || error);
}
}
@ApiOkResponse({ type: ProductRes })
@Post('/')
async createProduct(@Body() productData: CreateProductDTO) {
@ -117,7 +142,7 @@ export class ProductController {
const file = files?.[0];
if (!file) return errorResponse('未接收到上传文件');
const result = await this.productService.importProductsCSV(file);
const result = await this.productService.importProductsFromTable(file);
return successResponse(result);
} catch (error) {
return errorResponse(error?.message || error);
@ -698,10 +723,10 @@ export class ProductController {
// 从站点同步产品到本地
@ApiOkResponse({ description: '从站点同步产品到本地', type: ProductRes })
@Post('/sync-from-site')
async syncProductFromSite(@Body() body: { siteId: number; siteProductId: string | number }) {
async syncProductFromSite(@Body() body: { siteId: number; siteProductId: string | number ,sku: string}) {
try {
const { siteId, siteProductId } = body;
const product = await this.productService.syncProductFromSite(siteId, siteProductId);
const { siteId, siteProductId, sku } = body;
const product = await this.productService.syncProductFromSite(siteId, siteProductId, sku);
return successResponse(product);
} catch (error) {
return errorResponse(error?.message || error);
@ -713,25 +738,26 @@ export class ProductController {
@Post('/batch-sync-from-site')
async batchSyncFromSite(@Body() body: { siteId: number; siteProductIds: (string | number)[] }) {
try {
const { siteId, siteProductIds } = body;
const result = await this.productService.batchSyncFromSite(siteId, siteProductIds);
// 将服务层返回的结果转换为统一格式
const errors = result.errors.map((error: string) => {
// 提取产品ID部分作为标识符
const match = error.match(/站点产品ID (\d+) /);
const identifier = match ? match[1] : 'unknown';
return {
identifier: identifier,
error: error
};
});
throw new Error('批量同步产品到本地暂未实现');
// const { siteId, siteProductIds } = body;
// const result = await this.productService.batchSyncFromSite(siteId, siteProductIds.map((id) => ({ siteProductId: id, sku: '' })));
// // 将服务层返回的结果转换为统一格式
// const errors = result.errors.map((error: string) => {
// // 提取产品ID部分作为标识符
// const match = error.match(/站点产品ID (\d+) /);
// const identifier = match ? match[1] : 'unknown';
// return {
// identifier: identifier,
// error: error
// };
// });
return successResponse({
total: siteProductIds.length,
processed: result.synced + errors.length,
synced: result.synced,
errors: errors
});
// return successResponse({
// total: siteProductIds.length,
// processed: result.synced + errors.length,
// synced: result.synced,
// errors: errors
// });
} catch (error) {
return errorResponse(error?.message || error);
}
@ -749,4 +775,31 @@ export class ProductController {
return errorResponse(error?.message || error);
}
}
// 获取所有产品,支持按品牌过滤
@ApiOkResponse({ description: '获取所有产品', type: ProductListRes })
@Get('/all')
async getAllProducts(@Query('brand') brand?: string) {
try {
const data = await this.productService.getAllProducts(brand);
return successResponse(data);
} catch (error) {
return errorResponse(error?.message || error);
}
}
// 获取按属性分组的产品,默认按强度划分
@ApiOkResponse({ description: '获取按属性分组的产品' })
@Get('/grouped')
async getGroupedProducts(
@Query('brand') brand?: string,
@Query('attribute') attribute: string = 'strength'
) {
try {
const data = await this.productService.getProductsGroupedByAttribute(brand, attribute);
return successResponse(data);
} catch (error) {
return errorResponse(error?.message || error);
}
}
}

View File

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

View File

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

View File

@ -12,10 +12,9 @@ import * as crypto from 'crypto';
import { SiteService } from '../service/site.service';
import { OrderService } from '../service/order.service';
import { SiteApiService } from '../service/site-api.service';
import {
UnifiedOrderDTO,
} from '../dto/site-api.dto';
@Controller('/webhook')
export class WebhookController {
@ -34,6 +33,8 @@ export class WebhookController {
@Inject()
private readonly siteService: SiteService;
@Inject()
private readonly siteApiService: SiteApiService;
// 移除配置中的站点数组,来源统一改为数据库
@ -49,7 +50,7 @@ export class WebhookController {
@Query('siteId') siteIdStr: string,
@Headers() header: any
) {
console.log(`webhook woocommerce`, siteIdStr, body,header)
console.log(`webhook woocommerce`, siteIdStr, body, header)
const signature = header['x-wc-webhook-signature'];
const topic = header['x-wc-webhook-topic'];
const source = header['x-wc-webhook-source'];
@ -79,7 +80,14 @@ export class WebhookController {
.update(rawBody)
.digest('base64');
try {
if (hash === signature) {
if (hash !== signature) {
return {
code: 403,
success: false,
message: 'Webhook verification failed',
};
}
const adapter = await this.siteApiService.getAdapter(siteId);
switch (topic) {
case 'product.created':
case 'product.updated':
@ -90,7 +98,8 @@ export class WebhookController {
break;
case 'order.created':
case 'order.updated':
await this.orderService.syncSingleOrder(siteId, body);
const order = adapter.mapPlatformToUnifiedOrder(body)
await this.orderService.syncSingleOrder(siteId, order);
break;
case 'order.deleted':
break;
@ -102,19 +111,12 @@ export class WebhookController {
break;
default:
console.log('Unhandled event:', body.event);
}
return {
code: 200,
success: true,
message: 'Webhook processed successfully',
};
} else {
return {
code: 403,
success: false,
message: 'Webhook verification failed',
};
}
} catch (error) {
console.log(error);
@ -130,23 +132,10 @@ export class WebhookController {
@Query('signature') signature: string,
@Headers() header: any
) {
console.log(`webhook shoppy`, siteIdStr, body, header)
const topic = header['x-oemsaas-event-type'];
// const source = header['x-oemsaas-shop-domain'];
const siteId = Number(siteIdStr);
const bodys = new UnifiedOrderDTO();
Object.assign(bodys, body);
// 从数据库获取站点配置
const site = await this.siteService.get(siteId, true);
// if (!site || !source?.includes(site.websiteUrl)) {
if (!site) {
console.log('domain not match');
return {
code: HttpStatus.BAD_REQUEST,
success: false,
message: 'domain not match',
};
}
if (!signature) {
return {
@ -162,6 +151,7 @@ export class WebhookController {
// .createHmac('sha256', this.secret)
// .update(rawBody)
// .digest('base64');
const adapter = await this.siteApiService.getAdapter(siteId);
try {
if (this.secret === signature) {
switch (topic) {
@ -174,7 +164,8 @@ export class WebhookController {
break;
case 'orders/create':
case 'orders/update':
await this.orderService.syncSingleOrder(siteId, bodys);
const order = adapter.mapPlatformToUnifiedOrder(body)
await this.orderService.syncSingleOrder(siteId, order);
break;
case 'orders/delete':
break;
@ -193,15 +184,10 @@ export class WebhookController {
success: true,
message: 'Webhook processed successfully',
};
} else {
return {
code: 403,
success: false,
message: 'Webhook verification failed',
};
}
} catch (error) {
console.log(error);
}
}
}

View File

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

View File

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

View File

@ -50,13 +50,37 @@ export class UnifiedSearchParamsDTO<Where=Record<string, any>> {
required: false,
})
orderBy?: Record<string, 'asc' | 'desc'> | string;
@ApiProperty({
description: '分组字段,例如 "categoryId"',
type: 'string',
required: false,
})
groupBy?: string;
}
/**
* Shopyy获取所有订单参数DTO
*/
export class ShopyyGetAllOrdersParams {
@ApiProperty({ description: '每页数量', example: 100, required: false })
per_page?: number;
@ApiProperty({ description: '支付时间范围开始', example: '2023-01-01T00:00:00Z', required: false })
pay_at_min?: string;
@ApiProperty({ description: '支付时间范围结束', example: '2023-01-01T23:59:59Z', required: false })
pay_at_max?: string;
@ApiProperty({ description: '排序字段', example: 'id', required: false })
order_field?: string;//排序字段默认id id=订单ID updated_at=最后更新时间 pay_at=支付时间
}
/**
*
*/
export interface BatchErrorItem {
// 错误项标识可以是ID、邮箱等
// 错误项标识(可以是ID、邮箱等)
identifier: string;
// 错误信息
error: string;
@ -76,7 +100,7 @@ export interface BatchOperationResult {
updated?: number;
// 删除数量
deleted?: number;
// 跳过的数量(如数据已存在或无需处理)
// 跳过的数量(如数据已存在或无需处理)
skipped?: number;
// 错误列表
errors: BatchErrorItem[];
@ -101,7 +125,7 @@ export class SyncOperationResult implements BatchOperationResult {
* DTO
*/
export class BatchErrorItemDTO {
@ApiProperty({ description: '错误项标识如ID、邮箱等', type: String })
@ApiProperty({ description: '错误项标识(如ID、邮箱等)', type: String })
@Rule(RuleType.string().required())
identifier: string;
@ -164,7 +188,7 @@ export class SyncParamsDTO {
@Rule(RuleType.string().optional())
endDate?: string;
@ApiProperty({ description: '强制同步(忽略缓存)', type: Boolean, required: false, default: false })
@ApiProperty({ description: '强制同步(忽略缓存)', type: Boolean, required: false, default: false })
@Rule(RuleType.boolean().optional())
force?: boolean = false;
}
@ -183,7 +207,7 @@ export class BatchQueryDTO {
}
/**
*
* ()
*/
export class BatchOperationResultDTOGeneric<T> extends BatchOperationResultDTO {
@ApiProperty({ description: '操作成功的数据列表', type: Array })
@ -191,7 +215,7 @@ export class BatchOperationResultDTOGeneric<T> extends BatchOperationResultDTO {
}
/**
*
* ()
*/
export class SyncOperationResultDTOGeneric<T> extends SyncOperationResultDTO {
@ApiProperty({ description: '同步成功的数据列表', type: Array })

View File

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

View File

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

View File

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

View File

@ -98,14 +98,10 @@ export class QueryOrderDTO {
}
export class QueryOrderSalesDTO {
@ApiProperty()
@ApiProperty({ description: '是否为原产品还是库存产品' })
@Rule(RuleType.bool().default(false))
isSource: boolean;
@ApiProperty()
@Rule(RuleType.bool().default(false))
exceptPackage: boolean;
@ApiProperty({ example: '1', description: '页码' })
@Rule(RuleType.number())
current: number;
@ -114,19 +110,31 @@ export class QueryOrderSalesDTO {
@Rule(RuleType.number())
pageSize: number;
@ApiProperty()
@ApiProperty({ description: '排序对象,格式如 { productName: "asc", sku: "desc" }',type: 'any', required: false })
@Rule(RuleType.object().allow(null))
orderBy?: Record<string, 'asc' | 'desc'>;
// filter
@ApiProperty({ description: '是否排除套餐' })
@Rule(RuleType.bool().default(false))
exceptPackage: boolean;
@ApiProperty({ description: '站点ID' })
@Rule(RuleType.number())
siteId: number;
@ApiProperty()
@ApiProperty({ description: '名称' })
@Rule(RuleType.string())
name: string;
@ApiProperty()
@ApiProperty({ description: 'SKU' })
@Rule(RuleType.string())
sku: string;
@ApiProperty({ description: '开始日期' })
@Rule(RuleType.date())
startDate: Date;
@ApiProperty()
@ApiProperty({ description: '结束日期' })
@Rule(RuleType.date())
endDate: Date;
}

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -2,6 +2,8 @@ import { ApiProperty } from '@midwayjs/swagger';
import {
UnifiedPaginationDTO,
} from './api.dto';
import { Dict } from '../entity/dict.entity';
import { Product } from '../entity/product.entity';
// export class UnifiedOrderWhere{
// []
// }
@ -17,6 +19,29 @@ export enum OrderFulfillmentStatus {
// 确认发货
CONFIRMED,
}
export enum OrderPaymentStatus {
// 待支付
PENDING,
// 支付中
PAYING,
// 部分支付
PARTIALLY_PAID,
// 已支付
PAID,
// 支付失败
FAILED,
// 部分退款
PARTIALLY_REFUNDED,
// 已退款
REFUNDED,
// 已取消
CANCELLED,
}
//
export class UnifiedProductWhere {
sku?: string;
[prop:string]:any
}
export class UnifiedTagDTO {
// 标签DTO用于承载统一标签数据
@ApiProperty({ description: '标签ID' })
@ -135,8 +160,10 @@ export class UnifiedProductAttributeDTO {
@ApiProperty({ description: '属性选项', type: [String] })
options: string[];
@ApiProperty({ description: '变体属性值(单个值)', required: false })
@ApiProperty({ description: '变体属性值(单个值)', required: false })
option?: string;
// 这个是属性的父级字典项
dict?: Dict;
}
export class UnifiedProductVariationDTO {
@ -280,17 +307,7 @@ export class UnifiedProductDTO {
type: 'object',
required: false,
})
erpProduct?: {
id: number;
sku: string;
name: string;
nameCn?: string;
category?: any;
attributes?: any[];
components?: any[];
price: number;
promotionPrice: number;
};
erpProduct?: Product
}
export class UnifiedOrderRefundDTO {
@ -782,17 +799,20 @@ export class UpdateWebhookDTO {
export class FulfillmentItemDTO {
@ApiProperty({ description: '订单项ID' })
@ApiProperty({ description: '订单项ID' ,required: false})
order_item_id: number;
@ApiProperty({ description: '数量' })
@ApiProperty({ description: '数量' ,required:false})
quantity: number;
}
export class FulfillmentDTO {
@ApiProperty({ description: '物流id', required: false })
tracking_id?: string;
@ApiProperty({ description: '物流单号', required: false })
tracking_number?: string;
@ApiProperty({ description: "物流产品代码" , required: false})
tracking_product_code?: string;
@ApiProperty({ description: '物流公司', required: false })
shipping_provider?: string;

View File

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

View File

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

View File

@ -8,11 +8,11 @@ export interface WooProduct {
id: number;
// 创建时间
date_created: string;
// 创建时间GMT
// 创建时间(GMT)
date_created_gmt: string;
// 更新时间
date_modified: string;
// 更新时间GMT
// 更新时间(GMT)
date_modified_gmt: string;
// 产品类型 simple grouped external variable
type: string;
@ -20,7 +20,7 @@ export interface WooProduct {
status: string;
// 是否为特色产品
featured: boolean;
// 目录可见性选项visible, catalog, search and hidden. Default is visible.
// 目录可见性选项:visible, catalog, search and hidden. Default is visible.
catalog_visibility: string;
// 常规价格
@ -117,9 +117,9 @@ export interface WooProduct {
// 购买备注
purchase_note?: string;
// 分类列表
categories?: Array<{ id: number; name?: string; slug?: string }>;
categories?: Array<{ id?: number; name?: string; slug?: string }>;
// 标签列表
tags?: Array<{ id: number; name?: string; slug?: string }>;
tags?: Array<{ id?: number; name?: string; slug?: string }>;
// 菜单排序
menu_order?: number;
// 元数据
@ -130,11 +130,11 @@ export interface WooVariation {
id: number;
// 创建时间
date_created: string;
// 创建时间GMT
// 创建时间(GMT)
date_created_gmt: string;
// 更新时间
date_modified: string;
// 更新时间GMT
// 更新时间(GMT)
date_modified_gmt: string;
// 变体描述
description: string;
@ -150,11 +150,11 @@ export interface WooVariation {
price_html?: string;
// 促销开始日期
date_on_sale_from?: string;
// 促销开始日期GMT
// 促销开始日期(GMT)
date_on_sale_from_gmt?: string;
// 促销结束日期
date_on_sale_to?: string;
// 促销结束日期GMT
// 促销结束日期(GMT)
date_on_sale_to_gmt?: string;
// 是否在促销中
on_sale: boolean;
@ -369,18 +369,35 @@ export interface WooOrder {
date_created_gmt?: string;
date_modified?: string;
date_modified_gmt?: string;
// 物流追踪信息
fulfillments?: Array<{
tracking_number?: string;
shipping_provider?: string;
shipping_method?: string;
status?: string;
date_created?: string;
items?: Array<{
order_item_id?: number;
quantity?: number;
}>;
}>;
}
export interface MetaDataFulfillment {
custom_tracking_link: string;
custom_tracking_provider: string;
date_shipped: number;
source: string;
status_shipped: string;
tracking_id: string;
tracking_number: string;
tracking_product_code: string;
tracking_provider: string;
user_id: number;
}
// 这个是一个插件的物流追踪信息
// 接口:
export interface WooFulfillment {
data_sipped: string;
tracking_id: string;
tracking_link: string;
tracking_number: string;
tracking_provider: string;
}
// https://docs.zorem.com/docs/ast-free/developers/adding-tracking-info-to-orders/
export interface WooFulfillmentCreateParams {
order_id: string;
tracking_provider: string;
tracking_number: string;
date_shipped?: string;
status_shipped?: string;
}
export interface WooOrderRefund {
id?: number;
@ -552,7 +569,8 @@ export interface WooOrderSearchParams {
order: string;
orderby: string;
parant: string[];
status: (WooOrderStatusSearchParams)[];
parent_exclude: string[];
status: WooOrderStatusSearchParams[];
customer: number;
product: number;
dp: number;
@ -616,6 +634,83 @@ export interface ListParams {
parant: string[];
parent_exclude: string[];
}
export interface WpMediaGetListParams {
// 请求范围,决定响应中包含的字段
// 默认: view
// 可选值: view, embed, edit
context?: 'view' | 'embed' | 'edit';
// 当前页码
// 默认: 1
page?: number;
// 每页最大返回数量
// 默认: 10
per_page?: number;
// 搜索字符串,限制结果匹配
search?: string;
// ISO8601格式日期限制发布时间之后的结果
after?: string;
// ISO8601格式日期限制修改时间之后的结果
modified_after?: string;
// 作者ID数组限制结果集为特定作者
author?: number[];
// 作者ID数组排除特定作者的结果
author_exclude?: number[];
// ISO8601格式日期限制发布时间之前的结果
before?: string;
// ISO8601格式日期限制修改时间之前的结果
modified_before?: string;
// ID数组排除特定ID的结果
exclude?: number[];
// ID数组限制结果集为特定ID
include?: number[];
// 结果集偏移量
offset?: number;
// 排序方向
// 默认: desc
// 可选值: asc, desc
order?: 'asc' | 'desc';
// 排序字段
// 默认: date
// 可选值: author, date, id, include, modified, parent, relevance, slug, include_slugs, title
orderby?: 'author' | 'date' | 'id' | 'include' | 'modified' | 'parent' | 'relevance' | 'slug' | 'include_slugs' | 'title';
// 父ID数组限制结果集为特定父ID
parent?: number[];
// 父ID数组排除特定父ID的结果
parent_exclude?: number[];
// 搜索的列名数组
search_columns?: string[];
// slug数组限制结果集为特定slug
slug?: string[];
// 状态数组,限制结果集为特定状态
// 默认: inherit
status?: string[];
// 媒体类型,限制结果集为特定媒体类型
// 可选值: image, video, text, application, audio
media_type?: 'image' | 'video' | 'text' | 'application' | 'audio';
// MIME类型限制结果集为特定MIME类型
mime_type?: string;
}
export enum WooContext {
view,
edit

View File

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

View File

@ -106,7 +106,7 @@ export class Order {
@Expose()
cart_tax: number;
@ApiProperty()
@ApiProperty({ type: "总金额"})
@Column('decimal', { precision: 10, scale: 2, default: 0 })
@Expose()
total: number;
@ -272,6 +272,14 @@ export class Order {
@Expose()
updatedAt: Date;
@ApiProperty({ type: 'json', nullable: true, description: '订单项列表' })
@Expose()
orderItems?: any[];
@ApiProperty({ type: 'json', nullable: true, description: '销售项列表' })
@Expose()
orderSales?: any[];
// 在插入或更新前处理用户代理字符串
@BeforeInsert()
@BeforeUpdate()

View File

@ -1,8 +1,8 @@
import { ApiProperty } from '@midwayjs/swagger';
import { Exclude, Expose } from 'class-transformer';
import {
BeforeInsert,
BeforeUpdate,
// BeforeInsert,
// BeforeUpdate,
Column,
CreateDateColumn,
Entity,
@ -22,22 +22,27 @@ export class OrderSale {
@Expose()
id?: number;
@ApiProperty()
@ApiProperty({ name:'原始订单ID' })
@Column()
@Expose()
orderId: number; // 订单 ID
@ApiProperty()
@Column({ nullable: true })
@ApiProperty({ name:'站点' })
@Column()
@Expose()
siteId: number; // 来源站点唯一标识
@ApiProperty()
@ApiProperty({name: "原始订单 itemId"})
@Column({ nullable: true })
@Expose()
externalOrderItemId: string; // WooCommerce 订单item ID
@ApiProperty()
@ApiProperty({name: "父产品 ID"})
@Column({ nullable: true })
@Expose()
parentProductId?: number; // 父产品 ID 用于统计套餐 如果是单品则不记录
@ApiProperty({name: "产品 ID"})
@Column()
@Expose()
productId: number;
@ -50,7 +55,7 @@ export class OrderSale {
@ApiProperty({ description: 'sku', type: 'string' })
@Expose()
@Column()
sku: string;
sku: string;// 库存产品sku
@ApiProperty()
@Column()
@ -62,25 +67,40 @@ export class OrderSale {
@Expose()
isPackage: boolean;
@ApiProperty()
@Column({ default: false })
@ApiProperty({ description: '商品品类', type: 'string',nullable: true})
@Expose()
isYoone: boolean;
@Column({ nullable: true })
category?: string;
// TODO 这个其实还是直接保存 product 比较好
@ApiProperty({ description: '品牌', type: 'string',nullable: true})
@Expose()
@Column({ nullable: true })
brand?: string;
@ApiProperty()
@Column({ default: false })
@ApiProperty({ description: '口味', type: 'string', nullable: true })
@Expose()
isZex: boolean;
@Column({ nullable: true })
flavor?: string;
@ApiProperty({ nullable: true })
@Column({ type: 'int', nullable: true })
@ApiProperty({ description: '湿度', type: 'string', nullable: true })
@Expose()
size: number | null;
@Column({ nullable: true })
humidity?: string;
@ApiProperty()
@Column({ default: false })
@ApiProperty({ description: '尺寸', type: 'string', nullable: true })
@Expose()
isYooneNew: boolean;
@Column({ nullable: true })
size?: string;
@ApiProperty({name: '强度', nullable: true })
@Column({ nullable: true })
@Expose()
strength: string | null;
@ApiProperty({ description: '版本', type: 'string', nullable: true })
@Expose()
@Column({ nullable: true })
version?: string;
@ApiProperty({
example: '2022-12-12 11:11:11',
@ -97,25 +117,4 @@ export class OrderSale {
@UpdateDateColumn()
@Expose()
updatedAt?: Date;
// === 自动计算逻辑 ===
@BeforeInsert()
@BeforeUpdate()
setFlags() {
if (!this.name) return;
const lower = this.name.toLowerCase();
this.isYoone = lower.includes('yoone');
this.isZex = lower.includes('zex');
this.isYooneNew = this.isYoone && lower.includes('new');
let size: number | null = null;
const sizes = [3, 6, 9, 12, 15, 18];
for (const s of sizes) {
if (lower.includes(s.toString())) {
size = s;
break;
}
}
this.size = size;
}
}

View File

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

View File

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

View File

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

View File

@ -0,0 +1,86 @@
import {
Column,
CreateDateColumn,
UpdateDateColumn,
Entity,
ManyToOne,
JoinColumn,
} from 'typeorm';
import { ApiProperty } from '@midwayjs/swagger';
import { Site } from './site.entity';
import { Product } from './product.entity';
@Entity('site_product')
export class SiteProduct {
@ApiProperty({
example: '12345',
description: '站点商品ID',
type: 'string',
required: true,
})
@Column({ primary: true })
id: string;
@ApiProperty({ description: '站点ID' })
@Column()
siteId: number;
@ApiProperty({ description: '商品ID' })
@Column({ nullable: true })
productId: number;
@ApiProperty({ description: 'sku'})
@Column()
sku: string;
@ApiProperty({ description: '类型' })
@Column({ length: 16, default: 'single' })
type: string;
@ApiProperty({
description: '产品名称',
type: 'string',
required: true,
})
@Column()
name: string;
@ApiProperty({ description: '产品图片' })
@Column({ default: '' })
image: string;
@ApiProperty({ description: '父商品ID', example: '12345' })
@Column({ nullable: true })
parentId: string;
// 站点关联
@ManyToOne(() => Site, site => site.id)
@JoinColumn({ name: 'siteId' })
site: Site;
// 商品关联
@ManyToOne(() => Product, product => product.id)
@JoinColumn({ name: 'productId' })
product: Product;
// 父商品关联
@ManyToOne(() => SiteProduct, siteProduct => siteProduct.id)
@JoinColumn({ name: 'parentId' })
parent: SiteProduct;
@ApiProperty({
example: '2022-12-12 11:11:11',
description: '创建时间',
required: true,
})
@CreateDateColumn()
createdAt: Date;
@ApiProperty({
example: '2022-12-12 11:11:11',
description: '更新时间',
required: true,
})
@UpdateDateColumn()
updatedAt: Date;
}

View File

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

View File

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

View File

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

View File

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

40
src/job/sync_tms.job.ts Normal file
View File

@ -0,0 +1,40 @@
import { ILogger, Inject, Logger } from '@midwayjs/core';
import { IJob, Job } from '@midwayjs/cron';
import { LogisticsService } from '../service/logistics.service';
import { Repository } from 'typeorm';
import { Shipment } from '../entity/shipment.entity';
import { InjectEntityModel } from '@midwayjs/typeorm';
@Job({
cronTime: '0 0 12 * * *', // 每天12点执行
start: true
})
export class SyncTmsJob implements IJob {
@Logger()
logger: ILogger;
@Inject()
logisticsService: LogisticsService;
@InjectEntityModel(Shipment)
shipmentModel: Repository<Shipment>
async onTick() {
const shipments:Shipment[] = await this.shipmentModel.findBy({ tracking_provider: 'freightwaves',finished: false });
const results = await Promise.all(
shipments.map(async shipment => {
return await this.logisticsService.updateFreightwavesShipmentState(shipment);
})
)
this.logger.info(`更新运单状态完毕 ${JSON.stringify(results)}`);
return results
}
onComplete(result: any) {
this.logger.info(`更新运单状态完成 ${result}`);
}
onError(error: any) {
this.logger.error(`更新运单状态失败 ${error.message}`);
}
}

View File

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

View File

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

View File

@ -50,7 +50,7 @@ export class DictService {
}
// 从XLSX文件导入字典
async importDictsFromXLSX(bufferOrPath: Buffer | string) {
async importDictsFromTable(bufferOrPath: Buffer | string) {
// 判断传入的是 Buffer 还是文件路径字符串
let buffer: Buffer;
if (typeof bufferOrPath === 'string') {
@ -93,7 +93,7 @@ export class DictService {
// 从XLSX文件导入字典项
async importDictItemsFromXLSX(bufferOrPath: Buffer | string, dictId: number): Promise<BatchOperationResultDTO> {
if(!dictId){
if (!dictId) {
throw new Error("引入失败, 请输入字典 ID")
}
@ -216,10 +216,10 @@ export class DictService {
// 如果提供了 dictId,则只返回该字典下的项
if (params.dictId) {
return this.dictItemModel.find({ where });
return this.dictItemModel.find({ where, relations: ['dict'] });
}
// 否则,返回所有字典项
return this.dictItemModel.find();
return this.dictItemModel.find({ relations: ['dict'] });
}
// 创建新字典项
@ -239,7 +239,7 @@ export class DictService {
}
// 更新或创建字典项 (Upsert)
// 如果字典项已存在(根据 name 和 dictId 判断),则更新;否则创建新的
// 如果字典项已存在(根据 name 和 dictId 判断),则更新;否则创建新的
async upsertDictItem(dictId: number, itemData: {
name: string;
title: string;
@ -252,7 +252,7 @@ export class DictService {
// 格式化 name
const formattedName = this.formatName(itemData.name);
// 查找是否已存在该字典项(根据 name 和 dictId
// 查找是否已存在该字典项(根据 name 和 dictId)
const existingItem = await this.dictItemModel.findOne({
where: {
name: formattedName,

View File

@ -0,0 +1,332 @@
import { Inject, Provide } from '@midwayjs/core';
import axios from 'axios';
import * as crypto from 'crypto';
import dayjs = require('dayjs');
import utc = require('dayjs/plugin/utc');
import timezone = require('dayjs/plugin/timezone');
// 扩展dayjs功能
dayjs.extend(utc);
dayjs.extend(timezone);
// 全局参数配置接口
interface FreightwavesConfig {
appSecret: string;
apiBaseUrl: string;
partner: string;
}
// 地址信息接口
interface Address {
name: string;
phone: string;
company: string;
countryCode: string;
city: string;
state: string;
address1: string;
address2: string;
postCode: string;
zoneCode?: string;
countryName: string;
cityName: string;
stateName: string;
companyName: string;
}
// 包裹尺寸接口
interface Dimensions {
length: number;
width: number;
height: number;
lengthUnit: 'IN' | 'CM';
weight: number;
weightUnit: 'LB' | 'KG';
}
// 包裹信息接口
interface Package {
dimensions: Dimensions;
currency: string;
description: string;
}
// 申报信息接口
interface Declaration {
boxNo: string;
sku: string;
cnname: string;
enname: string;
declaredPrice: number;
declaredQty: number;
material: string;
intendedUse: string;
cweight: number;
hsCode: string;
battery: string;
}
// 费用试算请求接口
export interface RateTryRequest {
shipCompany: string;
partnerOrderNumber: string;
warehouseId?: string;
shipper: Address;
reciver: Address;
packages: Package[];
partner: string;
signService?: 0 | 1;
}
// 创建订单请求接口
interface CreateOrderRequest extends RateTryRequest {
declaration: Declaration;
}
// 查询订单请求接口
interface QueryOrderRequest {
partnerOrderNumber?: string;
shipOrderId?: string;
partner: string;
}
// 修改订单请求接口
interface ModifyOrderRequest extends CreateOrderRequest {
shipOrderId: string;
}
// 订单退款请求接口
interface RefundOrderRequest {
shipOrderId: string;
partner: string;
}
// 通用响应接口
interface FreightwavesResponse<T> {
code: string;
msg: string;
data: T;
}
// 费用试算响应数据接口
interface RateTryResponseData {
shipCompany: string;
channelCode: string;
totalAmount: number;
currency: string;
}
// 创建订单响应数据接口
interface CreateOrderResponseData {
msg: string;
data: any;
}
// 查询订单响应数据接口
interface QueryOrderResponseData {
thirdOrderId: string;
shipCompany: string;
expressFinish: 0 | 1 | 2;
expressFailMsg: string;
expressOrder: {
mainTrackingNumber: string;
labelPath: string[];
totalAmount: number;
currency: string;
balance: number;
};
partnerOrderNumber: string;
shipOrderId: string;
}
// 修改订单响应数据接口
interface ModifyOrderResponseData extends CreateOrderResponseData { }
// 订单退款响应数据接口
interface RefundOrderResponseData { }
@Provide()
export class FreightwavesService {
@Inject() logger;
// 默认配置
private config: FreightwavesConfig = {
appSecret: 'gELCHguGmdTLo!zfihfM91hae8G@9Sz23Mh6pHrt',
apiBaseUrl: 'http://tms.freightwaves.ca:8901',
partner: '25072621035200000060'
};
// 初始化配置
setConfig(config: Partial<FreightwavesConfig>): void {
this.config = { ...this.config, ...config };
}
// 生成签名
private generateSignature(body: any, date: string): string {
const bodyString = JSON.stringify(body);
const signatureStr = `${bodyString}${this.config.appSecret}${date}`;
return crypto.createHash('md5').update(signatureStr).digest('hex');
}
// 发送请求
private async sendRequest<T>(url: string, data: any): Promise<FreightwavesResponse<T>> {
try {
// 设置请求头 - 使用太平洋时间 (America/Los_Angeles)
const date = dayjs().tz('America/Los_Angeles').format('YYYY-MM-DD HH:mm:ss');
const headers = {
'Content-Type': 'application/json',
'requestDate': date,
'signature': this.generateSignature(data, date),
};
// 发送请求 - 临时禁用SSL证书验证以解决UNABLE_TO_VERIFY_LEAF_SIGNATURE错误
const response = await axios.post<FreightwavesResponse<T>>(
`${this.config.apiBaseUrl}${url}`,
data,
{
headers,
httpsAgent: new (require('https').Agent)({
rejectUnauthorized: false
})
}
);
// 记录响应信息
this.log(`Received response from: ${this.config.apiBaseUrl}${url}`, {
status: response.status,
statusText: response.statusText,
data: response.data
});
// 处理响应
if (response.data.code !== '00000200') {
this.log(`Freightwaves API error: ${response.data.msg}`, { url, data, response: response.data });
throw new Error(response.data.msg);
}
return response.data;
} catch (error) {
// 更详细的错误记录
if (error.response) {
// 请求已发送,服务器返回错误状态码
this.log(`Freightwaves API request failed with status: ${error.response.status}`, {
url,
data,
response: error.response.data,
status: error.response.status,
headers: error.response.headers
});
} else if (error.request) {
// 请求已发送,但没有收到响应
this.log(`Freightwaves API request no response received`, {
url,
data,
request: error.request
});
} else {
// 请求配置时发生错误
this.log(`Freightwaves API request configuration error: ${error.message}`, {
url,
data,
error: error.message
});
}
throw error;
}
}
/**
*
* @param params
* @returns
*/
async rateTry(params: Omit<RateTryRequest, 'partner'>): Promise<RateTryResponseData> {
const requestData: RateTryRequest = {
...params,
partner: this.config.partner,
};
const response = await this.sendRequest<RateTryResponseData>('/shipService/order/rateTry', requestData);
return response.data;
}
/**
*
* @param params
* @returns
*/
async createOrder(params: Omit<CreateOrderRequest, 'partner'>): Promise<CreateOrderResponseData> {
const requestData: CreateOrderRequest = {
...params,
partner: this.config.partner,
};
const response = await this.sendRequest<CreateOrderResponseData>('/shipService/order/createOrder', requestData);
return response;
}
/**
*
* @param params
* @returns
*/
async queryOrder(params: Omit<QueryOrderRequest, 'partner'>): Promise<QueryOrderResponseData> {
const requestData: QueryOrderRequest = {
...params,
partner: this.config.partner,
};
const response = await this.sendRequest<QueryOrderResponseData>('/shipService/order/queryOrder', requestData);
if (response.code !== '00000200') {
throw new Error(response.msg);
}
return response.data;
}
/**
*
* @param params
* @returns
*/
async modifyOrder(params: Omit<ModifyOrderRequest, 'partner'>): Promise<ModifyOrderResponseData> {
const requestData: ModifyOrderRequest = {
...params,
partner: this.config.partner,
};
const response = await this.sendRequest<ModifyOrderResponseData>('/shipService/order/modifyOrder', requestData);
return response.data;
}
/**
* 退
* @param params 退
* @returns 退
*/
async refundOrder(params: Omit<RefundOrderRequest, 'partner'>): Promise<RefundOrderResponseData> {
const requestData: RefundOrderRequest = {
...params,
partner: this.config.partner,
};
const response = await this.sendRequest<RefundOrderResponseData>('/shipService/order/refundOrder', requestData);
return response.data;
}
/**
* logger可能未定义的情况
* @param message
* @param data
*/
private log(message: string, data?: any) {
if (this.logger) {
this.logger.info(message, data);
} else {
// 如果logger未定义使用console输出
if (data) {
console.log(message, data);
} else {
console.log(message);
}
}
}
}

View File

@ -27,10 +27,12 @@ import { CanadaPostService } from './canadaPost.service';
import { OrderItem } from '../entity/order_item.entity';
import { OrderSale } from '../entity/order_sale.entity';
import { UniExpressService } from './uni_express.service';
import { FreightwavesService, RateTryRequest } from './freightwaves.service';
import { StockPoint } from '../entity/stock_point.entity';
import { OrderService } from './order.service';
import { convertKeysFromCamelToSnake } from '../utils/object-transform.util';
import { SiteService } from './site.service';
import { ShopyyService } from './shopyy.service';
@Provide()
export class LogisticsService {
@ -73,9 +75,15 @@ export class LogisticsService {
@Inject()
uniExpressService: UniExpressService;
@Inject()
freightwavesService: FreightwavesService;
@Inject()
wpService: WPService;
@Inject()
shopyyService: ShopyyService;
@Inject()
orderService: OrderService;
@ -125,6 +133,10 @@ export class LogisticsService {
try {
const data = await this.uniExpressService.getOrderStatus(shipment.return_tracking_number);
console.log('updateShipmentState data:', data);
// huo
if (data.status === 'FAIL') {
throw new Error('获取运单状态失败,原因为' + data.ret_msg)
}
shipment.state = data.data[0].state;
if (shipment.state in [203, 215, 216, 230]) { // todo,写常数
shipment.finished = true;
@ -137,6 +149,30 @@ export class LogisticsService {
}
}
//"expressFinish": 0, //是否快递创建完成1完成 0未完成需要轮询 2:失败)
async updateFreightwavesShipmentState(shipment: Shipment) {
try {
const data = await this.freightwavesService.queryOrder({ shipOrderId: shipment.order_id.toString() });
console.log('updateFreightwavesShipmentState data:', data);
// huo
if (data.expressFinish === 2) {
throw new Error('获取运单状态失败,原因为' + data.expressFailMsg)
}
if (data.expressFinish === 0) {
shipment.state = '203';
shipment.finished = true;
}
this.shipmentModel.save(shipment);
return shipment.state;
} catch (error) {
throw error;
// throw new Error(`更新运单状态失败 ${error.message}`);
}
}
async updateShipmentStateById(id: number) {
const shipment: Shipment = await this.shipmentModel.findOneBy({ id: id });
return this.updateShipmentState(shipment);
@ -243,8 +279,7 @@ export class LogisticsService {
shipmentRepo.remove(shipment);
const res = await this.uniExpressService.deleteShipment(shipment.return_tracking_number);
console.log('res', res.data); // todo
await this.uniExpressService.deleteShipment(shipment.return_tracking_number);
await orderRepo.save(order);
@ -274,7 +309,6 @@ export class LogisticsService {
console.log('同步到woocommerce失败', error);
return true;
}
return true;
} catch {
throw new Error('删除运单失败');
@ -290,7 +324,16 @@ export class LogisticsService {
currency: 'CAD',
// item_description: data.sales, // todo: 货品信息
}
const resShipmentFee = await this.uniExpressService.getRates(reqBody);
let resShipmentFee: any;
if (data.shipmentPlatform === 'uniuni') {
resShipmentFee = await this.uniExpressService.getRates(reqBody);
} else if (data.shipmentPlatform === 'freightwaves') {
const fre_reqBody = await this.convertToFreightwavesRateTry(data);
resShipmentFee = await this.freightwavesService.rateTry(fre_reqBody);
} else {
throw new Error('不支持的运单平台');
}
if (resShipmentFee.status !== 'SUCCESS') {
throw new Error(resShipmentFee.ret_msg);
}
@ -315,40 +358,10 @@ export class LogisticsService {
let resShipmentOrder;
try {
const stock_point = await this.stockPointModel.findOneBy({ id: data.stockPointId });
const reqBody = {
sender: data.details.origin.contact_name,
start_phone: data.details.origin.phone_number,
start_postal_code: data.details.origin.address.postal_code.replace(/\s/g, ''),
pickup_address: data.details.origin.address.address_line_1,
pickup_warehouse: stock_point.upStreamStockPointId,
shipper_country_code: data.details.origin.address.country,
receiver: data.details.destination.contact_name,
city: data.details.destination.address.city,
province: data.details.destination.address.region,
country: data.details.destination.address.country,
postal_code: data.details.destination.address.postal_code.replace(/\s/g, ''),
delivery_address: data.details.destination.address.address_line_1,
receiver_phone: data.details.destination.phone_number.number,
receiver_email: data.details.destination.email_addresses,
// item_description: data.sales, // todo: 货品信息
length: data.details.packaging_properties.packages[0].measurements.cuboid.l,
width: data.details.packaging_properties.packages[0].measurements.cuboid.w,
height: data.details.packaging_properties.packages[0].measurements.cuboid.h,
dimension_uom: data.details.packaging_properties.packages[0].measurements.cuboid.unit,
weight: data.details.packaging_properties.packages[0].measurements.weight.value,
weight_uom: data.details.packaging_properties.packages[0].measurements.weight.unit,
currency: 'CAD',
custom_field: {
'order_id': order.externalOrderId
}
}
resShipmentOrder = await this.mepShipment(data, order);
// 添加运单
resShipmentOrder = await this.uniExpressService.createShipment(reqBody);
// 记录物流信息,并将订单状态转到完成
if (resShipmentOrder.status === 'SUCCESS') {
// 记录物流信息,并将订单状态转到完成,uniuni状态为SUCCESStms.freightwaves状态为00000200
if (resShipmentOrder.status === 'SUCCESS' || resShipmentOrder.code === '00000200') {
order.orderStatus = ErpOrderStatus.COMPLETED;
} else {
throw new Error('运单生成失败');
@ -359,12 +372,27 @@ export class LogisticsService {
await dataSource.transaction(async manager => {
const orderRepo = manager.getRepository(Order);
const shipmentRepo = manager.getRepository(Shipment);
const tracking_provider = 'UniUni'; // todo: id未确定,后写进常数
const tracking_provider = data.shipmentPlatform; // todo: id未确定,后写进常数
// 同步物流信息到woocommerce
const site = await this.siteService.get(Number(order.siteId), true);
let co: any;
let unique_id: any;
let state: any;
if (data.shipmentPlatform === 'uniuni') {
co = resShipmentOrder.data.tno;
unique_id = resShipmentOrder.data.uni_order_sn;
state = resShipmentOrder.data.uni_status_code;
} else {
co = resShipmentOrder.data?.shipOrderId;
unique_id = resShipmentOrder.data?.shipOrderId;
state = ErpOrderStatus.COMPLETED;
}
// 同步订单状态到woocommerce
if (order.source_type != "shopyy") {
const res = await this.wpService.createFulfillment(site, order.externalOrderId, {
tracking_number: resShipmentOrder.data.tno,
tracking_number: co,
tracking_provider: tracking_provider,
});
@ -372,36 +400,61 @@ export class LogisticsService {
const shipment = await shipmentRepo.save({
tracking_provider: tracking_provider,
tracking_id: res.data.tracking_id,
unique_id: resShipmentOrder.data.uni_order_sn,
unique_id: unique_id,
stockPointId: String(data.stockPointId), // todo
state: resShipmentOrder.data.uni_status_code,
return_tracking_number: resShipmentOrder.data.tno,
state: state,
return_tracking_number: co,
fee: data.details.shipmentFee,
order: order
});
order.shipmentId = shipment.id;
shipmentId = shipment.id;
}
// 同步订单状态到woocommerce
if (order.status !== OrderStatus.COMPLETED) {
await this.wpService.updateOrder(site, order.externalOrderId, {
status: OrderStatus.COMPLETED,
});
order.status = OrderStatus.COMPLETED;
}
}
if (order.source_type === "shopyy") {
const res = await this.shopyyService.createFulfillment(site, order.externalOrderId, {
tracking_number: co,
tracking_company: resShipmentOrder.shipCompany,
carrier_code: resShipmentOrder.shipperOrderId,
});
if (order.orderStatus === ErpOrderStatus.COMPLETED) {
const shipment = await shipmentRepo.save({
tracking_provider: tracking_provider,
tracking_id: res.data.tracking_id,
unique_id: unique_id,
stockPointId: String(data.stockPointId), // todo
state: state,
return_tracking_number: co,
fee: data.details.shipmentFee,
order: order
});
order.shipmentId = shipment.id;
shipmentId = shipment.id;
}
if (order.status !== OrderStatus.COMPLETED) {
// shopyy未提供更新订单接口暂不更新订单状态
// await this.shopyyService.updateOrder(site, order.externalOrderId, {
// status: OrderStatus.COMPLETED,
// });
order.status = OrderStatus.COMPLETED;
}
}
order.orderStatus = ErpOrderStatus.COMPLETED;
await orderRepo.save(order);
}).catch(error => {
transactionError = error
throw new Error(`请求错误:${error}`);
});
if (transactionError !== undefined) {
console.log('err', transactionError);
throw transactionError;
}
// 更新产品发货信息
this.orderService.updateOrderSales(order.id, sales);
@ -638,4 +691,190 @@ export class LogisticsService {
return { items, total, current, pageSize };
}
async mepShipment(data: ShipmentBookDTO, order: Order) {
try {
const stock_point = await this.stockPointModel.findOneBy({ id: data.stockPointId });
let resShipmentOrder;
if (data.shipmentPlatform === 'uniuni') {
const reqBody = {
sender: data.details.origin.contact_name,
start_phone: data.details.origin.phone_number,
start_postal_code: data.details.origin.address.postal_code.replace(/\s/g, ''),
pickup_address: data.details.origin.address.address_line_1,
pickup_warehouse: stock_point.upStreamStockPointId,
shipper_country_code: data.details.origin.address.country,
receiver: data.details.destination.contact_name,
city: data.details.destination.address.city,
province: data.details.destination.address.region,
country: data.details.destination.address.country,
postal_code: data.details.destination.address.postal_code.replace(/\s/g, ''),
delivery_address: data.details.destination.address.address_line_1,
receiver_phone: data.details.destination.phone_number.number,
receiver_email: data.details.destination.email_addresses,
// item_description: data.sales, // todo: 货品信息
length: data.details.packaging_properties.packages[0].measurements.cuboid.l,
width: data.details.packaging_properties.packages[0].measurements.cuboid.w,
height: data.details.packaging_properties.packages[0].measurements.cuboid.h,
dimension_uom: data.details.packaging_properties.packages[0].measurements.cuboid.unit,
weight: data.details.packaging_properties.packages[0].measurements.weight.value,
weight_uom: data.details.packaging_properties.packages[0].measurements.weight.unit,
currency: 'CAD',
custom_field: {
'order_id': order.externalOrderId // todo: 需要获取订单的externalOrderId
}
};
// 添加运单
resShipmentOrder = await this.uniExpressService.createShipment(reqBody);
}
if (data.shipmentPlatform === 'freightwaves') {
// 根据TMS系统对接说明文档格式化参数
const reqBody: any = {
shipCompany: 'UPSYYZ7000NEW',
partnerOrderNumber: order.siteId + '-' + order.externalOrderId,
warehouseId: '25072621030107400060',
shipper: {
name: data.details.origin.contact_name, // 姓名
phone: data.details.origin.phone_number.number, // 电话提取number属性转换为字符串
company: '', // 公司
countryCode: data.details.origin.address.country, // 国家Code
city: data.details.origin.address.city, // 城市
state: data.details.origin.address.region, // 州/省Code两个字母缩写
address1: data.details.origin.address.address_line_1, // 详细地址
address2: '', // 详细地址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,6 +39,7 @@ import * as path from 'path';
import * as os from 'os';
import { UnifiedOrderDTO } from '../dto/site-api.dto';
import { CustomerService } from './customer.service';
import { ProductService } from './product.service';
@Provide()
export class OrderService {
@ -110,6 +111,8 @@ export class OrderService {
@Logger()
logger; // 注入 Logger 实例
@Inject()
productService: ProductService;
/**
*
@ -138,7 +141,7 @@ export class OrderService {
updated: 0,
errors: []
};
this.logger.info('开始进入循环同步订单', result.length, '个订单')
// 遍历每个订单进行同步
for (const order of result) {
try {
@ -146,8 +149,8 @@ export class OrderService {
const existingOrder = await this.orderModel.findOne({
where: { externalOrderId: String(order.id), siteId: siteId },
});
if(!existingOrder){
console.log("数据库中不存在",order.id, '订单状态:', order.status )
if (!existingOrder) {
this.logger.debug("数据库中不存在", order.id, '订单状态:', order.status)
}
// 同步单个订单
await this.syncSingleOrder(siteId, order);
@ -162,6 +165,7 @@ export class OrderService {
} else {
syncResult.created++;
}
// console.log('updated', syncResult.updated, 'created:', syncResult.created)
} catch (error) {
// 记录错误但不中断整个同步过程
syncResult.errors.push({
@ -171,7 +175,7 @@ export class OrderService {
syncResult.processed++;
}
}
this.logger.debug('syncOrders result', syncResult)
this.logger.info('同步完成', syncResult.updated, 'created:', syncResult.created)
return syncResult;
}
@ -202,14 +206,14 @@ export class OrderService {
try {
// 调用 WooCommerce API 获取订单
const adapter = await this.siteApiService.getAdapter(siteId);
const order = await adapter.getOrder(orderId);
const order = await adapter.getOrder({ id: orderId });
// 检查订单是否已存在,以区分创建和更新
const existingOrder = await this.orderModel.findOne({
where: { externalOrderId: String(order.id), siteId: siteId },
});
if(!existingOrder){
console.log("数据库不存在", siteId , "订单:",order.id, '订单状态:' + order.status )
if (!existingOrder) {
this.logger.debug("数据库不存在", siteId, "订单:", order.id, '订单状态:' + order.status)
}
// 同步单个订单
await this.syncSingleOrder(siteId, order, true);
@ -268,7 +272,7 @@ export class OrderService {
try {
const site = await this.siteService.get(siteId);
// 仅处理 WooCommerce 站点
if(site.type !== 'woocommerce'){
if (site.type !== 'woocommerce') {
return
}
// 将订单状态同步到 WooCommerce,然后切换至下一状态
@ -278,6 +282,11 @@ export class OrderService {
console.error('更新订单状态失败,原因为:', error)
}
}
async getOrderByExternalOrderId(siteId: number, externalOrderId: string) {
return await this.orderModel.findOne({
where: { externalOrderId: String(externalOrderId), siteId },
});
}
/**
*
* :
@ -301,7 +310,7 @@ export class OrderService {
* @param order
* @param forceUpdate
*/
async syncSingleOrder(siteId: number, order: any, forceUpdate = false) {
async syncSingleOrder(siteId: number, order: UnifiedOrderDTO, forceUpdate = false) {
// 从订单数据中解构出各个子项
let {
line_items,
@ -315,53 +324,50 @@ export class OrderService {
// console.log('同步进单个订单', order)
// 如果订单状态为 AUTO_DRAFT,则跳过处理
if (order.status === OrderStatus.AUTO_DRAFT) {
this.logger.debug('订单状态为 AUTO_DRAFT,跳过处理', siteId, order.id)
return;
}
// 检查数据库中是否已存在该订单
const existingOrder = await this.orderModel.findOne({
where: { externalOrderId: order.id, siteId: siteId },
where: { externalOrderId: String(order.id), siteId: siteId },
});
// 自动更新订单状态(如果需要)
await this.autoUpdateOrderStatus(siteId, order);
if(existingOrder){
if (existingOrder) {
// 矫正数据库中的订单数据
const updateData: any = { status: order.status };
if (this.canUpdateErpStatus(existingOrder.orderStatus)) {
updateData.orderStatus = this.mapOrderStatus(order.status);
updateData.orderStatus = this.mapOrderStatus(order.status as any);
}
// 更新
await this.orderModel.update({ externalOrderId: order.id, siteId: siteId }, updateData);
// 更新订单主数据
await this.orderModel.update({ externalOrderId: String(order.id), siteId: siteId }, updateData);
// 更新 fulfillments 数据
await this.saveOrderFulfillments({
siteId,
orderId: existingOrder.id,
externalOrderId:order.id,
externalOrderId: order.id,
fulfillments: fulfillments,
});
}
const externalOrderId = order.id;
const externalOrderId = String(order.id);
// 这里的 saveOrder 已经包括了创建订单和更新订单
let orderRecord: Order = await this.saveOrder(siteId, orderData);
// 如果订单从未完成变为完成状态,则更新库存
if (
existingOrder &&
existingOrder.orderStatus !== ErpOrderStatus.COMPLETED &&
orderRecord &&
orderRecord.orderStatus !== ErpOrderStatus.COMPLETED &&
orderData.status === OrderStatus.COMPLETED
) {
this.updateStock(existingOrder);
await this.updateStock(orderRecord);
// 不再直接返回,继续执行后续的更新操作
}
// 如果订单不可编辑且不强制更新,则跳过处理
if (existingOrder && !existingOrder.is_editable && !forceUpdate) {
return;
}
// 保存订单主数据
const orderRecord = await this.saveOrder(siteId, orderData);
const orderId = orderRecord.id;
// 保存订单项
await this.saveOrderItems({
siteId,
orderId,
externalOrderId,
externalOrderId: String(externalOrderId),
orderItems: line_items,
});
// 保存退款信息
@ -459,13 +465,14 @@ export class OrderService {
* @param order
* @returns
*/
async saveOrder(siteId: number, order: UnifiedOrderDTO): Promise<Order> {
// 这里 omit 是因为处理在外头了 其实 saveOrder 应该包括 savelineitems 等
async saveOrder(siteId: number, order: Omit<UnifiedOrderDTO, 'line_items' | 'refunds'>): Promise<Order> {
// 将外部订单ID转换为字符串
const externalOrderId = String(order.id)
delete order.id
// 创建订单实体对象
const entity = plainToClass(Order, {...order, externalOrderId, siteId});
const entity = plainToClass(Order, { ...order, externalOrderId, siteId });
// 检查数据库中是否已存在该订单
const existingOrder = await this.orderModel.findOne({
where: { externalOrderId, siteId: siteId },
@ -479,7 +486,7 @@ export class OrderService {
// 如果不能更新 ERP 状态,则保留原有的 orderStatus
entity.orderStatus = existingOrder.orderStatus;
}
// 更新订单数据(包括 shipping、billing 等字段)
// 更新订单数据(包括 shipping、billing 等字段)
await this.orderModel.update(existingOrder.id, entity);
entity.id = existingOrder.id;
return entity;
@ -708,6 +715,8 @@ export class OrderService {
*
* @param orderItem
*/
// TODO 这里存的是库存商品实际
// 所以叫做 orderInventoryItems 可能更合适
async saveOrderSale(orderItem: OrderItem) {
const currentOrderSale = await this.orderSaleModel.find({
where: {
@ -719,53 +728,56 @@ export class OrderService {
await this.orderSaleModel.delete(currentOrderSale.map(v => v.id));
}
if (!orderItem.sku) return;
// 从数据库查询产品,关联查询组件
const product = await this.productModel.findOne({
where: { siteSkus: Like(`%${orderItem.sku}%`) },
relations: ['components'],
});
const productDetail = await this.productService.getComponentDetailFromSiteSku({ sku: orderItem.sku, name: orderItem.name });
if (!product) return;
const orderSales: OrderSale[] = [];
if (product.components && product.components.length > 0) {
for (const comp of product.components) {
const baseProduct = await this.productModel.findOne({
where: { sku: comp.sku },
});
if (baseProduct) {
const orderSaleItem: OrderSale = plainToClass(OrderSale, {
orderId: orderItem.orderId,
siteId: orderItem.siteId,
externalOrderItemId: orderItem.externalOrderItemId,
productId: baseProduct.id,
name: baseProduct.name,
if (!productDetail || !productDetail.quantity) return;
const { product, quantity } = productDetail
const componentDetails: { product: Product, quantity: number }[] = product.components?.length > 0 ? await Promise.all(product.components.map(async comp => {
return {
product: await this.productModel.findOne({
where: { id: comp.productId },
}),
quantity: comp.quantity * orderItem.quantity,
sku: comp.sku,
isPackage: orderItem.name.toLowerCase().includes('package'),
});
orderSales.push(orderSaleItem);
}
}
} else {
const orderSaleItem: OrderSale = plainToClass(OrderSale, {
})) : [{ product, quantity }]
const orderSales: OrderSale[] = componentDetails.map(componentDetail => {
if (!componentDetail.product) return null
const attrsObj = this.productService.getAttributesObject(product.attributes)
const orderSale = plainToClass(OrderSale, {
orderId: orderItem.orderId,
siteId: orderItem.siteId,
externalOrderItemId: orderItem.externalOrderItemId,
productId: product.id,
name: product.name,
quantity: orderItem.quantity,
sku: product.sku,
isPackage: orderItem.name.toLowerCase().includes('package'),
externalOrderItemId: orderItem.externalOrderItemId,// 原始 itemId
parentProductId: product.id, // 父产品 ID 用于统计套餐 如果是单品则不记录
productId: componentDetail.product.id,
isPackage: product.type === 'bundle',// 这里是否是套餐取决于父产品
name: componentDetail.product.name,
quantity: componentDetail.quantity * orderItem.quantity,
sku: componentDetail.product.sku,
// 理论上直接存 product 的全部数据才是对的,因为这样我的数据才全面。
brand: attrsObj?.['brand']?.name,
version: attrsObj?.['version']?.name,
strength: attrsObj?.['strength']?.name,
flavor: attrsObj?.['flavor']?.name,
humidity: attrsObj?.['humidity']?.name,
size: attrsObj?.['size']?.name,
category: componentDetail.product.category.name,
});
orderSales.push(orderSaleItem);
}
return orderSale
}).filter(v => v !== null)
if (orderSales.length > 0) {
await this.orderSaleModel.save(orderSales);
}
}
// // extract stren
// extractNumberFromString(str: string): number {
// if (!str) return 0;
// const num = parseInt(str, 10);
// return isNaN(num) ? 0 : num;
// }
/**
* 退
@ -1234,13 +1246,13 @@ export class OrderService {
parameters.push(siteId);
}
if (startDate) {
sqlQuery += ` AND o.date_created >= ?`;
totalQuery += ` AND o.date_created >= ?`;
sqlQuery += ` AND o.date_paid >= ?`;
totalQuery += ` AND o.date_paid >= ?`;
parameters.push(startDate);
}
if (endDate) {
sqlQuery += ` AND o.date_created <= ?`;
totalQuery += ` AND o.date_created <= ?`;
sqlQuery += ` AND o.date_paid <= ?`;
totalQuery += ` AND o.date_paid <= ?`;
parameters.push(endDate);
}
// 支付方式筛选(使用参数化,避免SQL注入)
@ -1328,7 +1340,7 @@ export class OrderService {
// 添加分页到主查询
sqlQuery += `
GROUP BY o.id
ORDER BY o.date_created DESC
ORDER BY o.date_paid DESC
LIMIT ? OFFSET ?
`;
parameters.push(pageSize, (current - 1) * pageSize);
@ -1426,7 +1438,7 @@ export class OrderService {
* @param params
* @returns
*/
async getOrderSales({ siteId, startDate, endDate, current, pageSize, name, exceptPackage }: QueryOrderSalesDTO) {
async getOrderSales({ siteId, startDate, endDate, current, pageSize, name, exceptPackage, orderBy }: QueryOrderSalesDTO) {
const nameKeywords = name ? name.split(' ').filter(Boolean) : [];
const defaultStart = dayjs().subtract(30, 'day').startOf('day').format('YYYY-MM-DD HH:mm:ss');
const defaultEnd = dayjs().endOf('day').format('YYYY-MM-DD HH:mm:ss');
@ -1467,7 +1479,7 @@ export class OrderService {
}
let itemSql = `
SELECT os.productId, os.name, SUM(os.quantity) AS totalQuantity, COUNT(DISTINCT os.orderId) AS totalOrders
SELECT os.productId, os.name, os.sku, SUM(os.quantity) AS totalQuantity, COUNT(DISTINCT os.orderId) AS totalOrders
FROM order_sale os
INNER JOIN \`order\` o ON o.id = os.orderId
WHERE o.date_paid BETWEEN ? AND ?
@ -1489,7 +1501,7 @@ export class OrderService {
}
itemSql += nameCondition;
itemSql += `
GROUP BY os.productId, os.name
GROUP BY os.productId, os.name, os.sku
ORDER BY totalQuantity DESC
LIMIT ? OFFSET ?
`;
@ -1546,7 +1558,6 @@ export class OrderService {
GROUP BY os.productId
`;
console.log('------3.5-----', pcSql, pcParams, exceptPackage);
const pcResults = await this.orderSaleModel.query(pcSql, pcParams);
const pcMap = new Map<number, any>();
@ -1579,14 +1590,14 @@ export class OrderService {
`;
let yooneSql = `
SELECT
SUM(CASE WHEN os.isYoone = 1 AND os.size = 3 THEN os.quantity ELSE 0 END) AS yoone3Quantity,
SUM(CASE WHEN os.isYoone = 1 AND os.size = 6 THEN os.quantity ELSE 0 END) AS yoone6Quantity,
SUM(CASE WHEN os.isYoone = 1 AND os.size = 9 THEN os.quantity ELSE 0 END) AS yoone9Quantity,
SUM(CASE WHEN os.isYoone = 1 AND os.size = 12 THEN os.quantity ELSE 0 END) AS yoone12Quantity,
SUM(CASE WHEN os.isYooneNew = 1 AND os.size = 12 THEN os.quantity ELSE 0 END) AS yoone12QuantityNew,
SUM(CASE WHEN os.isYoone = 1 AND os.size = 15 THEN os.quantity ELSE 0 END) AS yoone15Quantity,
SUM(CASE WHEN os.isYoone = 1 AND os.size = 18 THEN os.quantity ELSE 0 END) AS yoone18Quantity,
SUM(CASE WHEN os.isZex = 1 THEN os.quantity ELSE 0 END) AS zexQuantity
SUM(CASE WHEN os.brand = 'yoone' AND os.strength = '3mg' THEN os.quantity ELSE 0 END) AS yoone3Quantity,
SUM(CASE WHEN os.brand = 'yoone' AND os.strength = '6mg' THEN os.quantity ELSE 0 END) AS yoone6Quantity,
SUM(CASE WHEN os.brand = 'yoone' AND os.strength = '9mg' THEN os.quantity ELSE 0 END) AS yoone9Quantity,
SUM(CASE WHEN os.brand = 'yoone' AND os.strength = '12mg' THEN os.quantity ELSE 0 END) AS yoone12Quantity,
SUM(CASE WHEN os.brand = 'yoone' AND os.strength = '12mg' THEN os.quantity ELSE 0 END) AS yoone12QuantityNew,
SUM(CASE WHEN os.brand = 'yoone' AND os.strength = '15mg' THEN os.quantity ELSE 0 END) AS yoone15Quantity,
SUM(CASE WHEN os.brand = 'yoone' AND os.strength = '18mg' THEN os.quantity ELSE 0 END) AS yoone18Quantity,
SUM(CASE WHEN os.brand = 'zex' THEN os.quantity ELSE 0 END) AS zexQuantity
FROM order_sale os
INNER JOIN \`order\` o ON o.id = os.orderId
WHERE o.date_paid BETWEEN ? AND ?
@ -1642,11 +1653,12 @@ export class OrderService {
* @returns
*/
async getOrderItems({
current,
pageSize,
siteId,
startDate,
endDate,
current,
pageSize,
sku,
name,
}: QueryOrderSalesDTO) {
const nameKeywords = name ? name.split(' ').filter(Boolean) : [];
@ -2471,7 +2483,7 @@ export class OrderService {
return await dataSource.transaction(async manager => {
// 准备查询条件
const whereCondition: any = {};
if(validIds.length > 0){
if (validIds.length > 0) {
whereCondition.id = In(validIds);
}
@ -2513,7 +2525,7 @@ export class OrderService {
const boxCount = items.reduce((total, item) => total + item.quantity, 0);
// 构建订单内容
const orderContent = items.map(item => `${item.name} (${item.sku || ''}) x ${item.quantity}`).join('; ');
const orderContent = items.map(item => `${item.name} x ${item.quantity}`).join('; ');
// 构建姓名地址
const shipping = order.shipping;
@ -2567,12 +2579,12 @@ export class OrderService {
* CSV格式
* @param {any[]} data
* @param {Object} options
* @param {string} [options.type='string'] 'string' | 'buffer'
* @param {string} [options.fileName] 使
* @param {string} [options.type='string'] :'string' | 'buffer'
* @param {string} [options.fileName] (使)
* @param {boolean} [options.writeFile=false]
* @returns {string|Buffer} type返回字符串或Buffer
*/
async exportToCsv(data: any[], options: { type?: 'string' | 'buffer'; fileName?: string; writeFile?: boolean } = {}): Promise<string | Buffer> {
async exportToCsv(data: any[], options: { type?: 'string' | 'buffer'; fileName?: string; writeFile?: boolean } = {}): Promise<string | Buffer> {
try {
// 检查数据是否为空
if (!data || data.length === 0) {
@ -2617,20 +2629,16 @@ async exportToCsv(data: any[], options: { type?: 'string' | 'buffer'; fileName?:
// 获取当前用户目录
const userHomeDir = os.homedir();
// 构建目标路径(下载目录)
// 构建目标路径(下载目录)
const downloadsDir = path.join(userHomeDir, 'Downloads');
// 确保下载目录存在
if (!fs.existsSync(downloadsDir)) {
fs.mkdirSync(downloadsDir, { recursive: true });
}
const filePath = path.join(downloadsDir, fileName);
// 写入文件
fs.writeFileSync(filePath, csvContent, 'utf8');
console.log(`数据已成功导出至 ${filePath}`);
return filePath;
}
@ -2641,11 +2649,83 @@ async exportToCsv(data: any[], options: { type?: 'string' | 'buffer'; fileName?:
return csvContent;
} catch (error) {
console.error('导出CSV时出错:', 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;
}
}
}
if (rightIndex !== -1) {
return s.substring(0, leftIndex) + s.substring(rightIndex + 1);
}
return s;
};
// 1. 处理每个分号前面的括号对
let result = str;
// 找出所有分号的位置
const semicolonIndices: number[] = [];
for (let i = 0; i < result.length; i++) {
if (result[i] === ';') {
semicolonIndices.push(i);
}
}
// 从后向前处理每个分号,避免位置变化影响后续处理
for (let i = semicolonIndices.length - 1; i >= 0; i--) {
const semicolonIndex = semicolonIndices[i];
// 从分号位置向前查找最近的左括号
let lastLeftParenIndex = -1;
for (let j = semicolonIndex - 1; j >= 0; j--) {
if (result[j] === '(') {
lastLeftParenIndex = j;
break;
}
}
// 如果找到左括号,删除该括号对及其内容
if (lastLeftParenIndex !== -1) {
result = removeParenthesesAt(result, lastLeftParenIndex);
}
}
// 2. 处理整个字符串的最后一个括号对
let lastLeftParenIndex = result.lastIndexOf('(');
if (lastLeftParenIndex !== -1) {
result = removeParenthesesAt(result, lastLeftParenIndex);
}
return result;
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,3 +1,6 @@
/**
* https://www.apizza.net/project/e114fb8e628e0f604379f5b26f0d8330/browse
*/
import { ILogger, Inject, Provide } from '@midwayjs/core';
import axios, { AxiosRequestConfig } from 'axios';
import * as fs from 'fs';
@ -5,16 +8,16 @@ import * as FormData from 'form-data';
import { SiteService } from './site.service';
import { Site } from '../entity/site.entity';
import { UnifiedReviewDTO } from '../dto/site-api.dto';
import { ShopyyReview } from '../dto/shopyy.dto';
import { ShopyyGetOneOrderResult, ShopyyReview } from '../dto/shopyy.dto';
import { BatchOperationDTO, BatchOperationResultDTO } from '../dto/batch.dto';
import { UnifiedSearchParamsDTO } from '../dto/api.dto';
import { UnifiedSearchParamsDTO, ShopyyGetAllOrdersParams } from '../dto/api.dto';
/**
* ShopYY平台服务实现
*/
@Provide()
export class ShopyyService {
@Inject()
logger:ILogger;
logger: ILogger;
/**
* ShopYY评论列表
* @param site
@ -125,7 +128,7 @@ export class ShopyyService {
* @returns URL
*/
private buildURL(baseUrl: string, endpoint: string): string {
// ShopYY API URL格式https://{shop}.shopyy.com/openapi/{version}/{endpoint}
// ShopYY API URL格式:https://{shop}.shopyy.com/openapi/{version}/{endpoint}
const base = baseUrl.replace(/\/$/, '');
const end = endpoint.replace(/^\//, '');
return `${base}/${end}`;
@ -155,7 +158,7 @@ export class ShopyyService {
* @param params
* @returns
*/
private async request(site: any, endpoint: string, method: string = 'GET', data: any = null, params: any = null): Promise<any> {
async request(site: any, endpoint: string, method: string = 'GET', data: any = null, params: any = null): Promise<any> {
const url = this.buildURL(site.apiUrl, endpoint);
const headers = this.buildHeaders(site);
@ -180,41 +183,19 @@ export class ShopyyService {
*
*/
public async fetchResourcePaged<T>(site: any, endpoint: string, params: Record<string, any> = {}) {
const page = Number(params.page || 1);
const limit = Number(params.per_page ?? 20);
const where = params.where && typeof params.where === 'object' ? params.where : {};
let orderby: string | undefined = params.orderby;
let order: 'asc' | 'desc' | undefined = params.orderDir as any;
if (!orderby && params.order && typeof params.order === 'object') {
const entries = Object.entries(params.order as Record<string, any>);
if (entries.length > 0) {
const [field, dir] = entries[0];
orderby = field;
order = String(dir).toLowerCase() === 'desc' ? 'desc' : 'asc';
const response = await this.request(site, endpoint, 'GET', null, params);
return this.mapPageResponse<T>(response, params);
}
}
// 映射统一入参到平台入参
const requestParams = {
...where,
...(params.search ? { search: params.search } : {}),
...(params.status ? { status: params.status } : {}),
...(orderby ? { orderby } : {}),
...(order ? { order } : {}),
page,
limit
};
this.logger.debug('ShopYY API请求分页参数:'+ JSON.stringify(requestParams));
const response = await this.request(site, endpoint, 'GET', null, requestParams);
mapPageResponse<T>(response: any, query: Record<string, any>) {
if (response?.code !== 0) {
throw new Error(response?.msg)
}
return {
items: (response.data.list || []) as T[],
total: response.data?.paginate?.total || 0,
totalPages: response.data?.paginate?.pageTotal || 0,
page: response.data?.paginate?.current || requestParams.page,
per_page: response.data?.paginate?.pagesize || requestParams.limit,
page: response.data?.paginate?.current || query.page,
per_page: response.data?.paginate?.pagesize || query.limit,
};
}
@ -225,13 +206,13 @@ export class ShopyyService {
* @param pageSize
* @returns
*/
async getProducts(site: any, page: number = 1, pageSize: number = 100): Promise<any> {
async getProducts(site: any, page: number = 1, pageSize: number = 100, where: Record<string, any> = {}): Promise<any> {
// ShopYY API: GET /products
// 通过 fields 参数指定需要返回的字段,确保 handle 等关键信息被包含
const response = await this.request(site, 'products', 'GET', null, {
page,
page_size: pageSize,
fields: 'id,name,sku,handle,status,type,stock_status,stock_quantity,images,regular_price,sale_price,tags,variations'
...where
});
return {
@ -291,7 +272,7 @@ export class ShopyyService {
const response = await this.request(site, `products/${productId}/variations/${variationId}`, 'GET');
return response.data;
}
mapOrderSearchParams(params: UnifiedSearchParamsDTO){
mapOrderSearchParams(params: UnifiedSearchParamsDTO) {
const { after, before, ...restParams } = params;
return {
...restParams,
@ -307,7 +288,7 @@ export class ShopyyService {
* @param pageSize
* @returns
*/
async getOrders(site: any | number, page: number = 1, pageSize: number = 100, params: UnifiedSearchParamsDTO = {}): Promise<any> {
async getOrders(site: any | number, page: number = 1, pageSize: number = 3000, params: ShopyyGetAllOrdersParams = {}): Promise<any> {
// 如果传入的是站点ID则获取站点配置
const siteConfig = typeof site === 'number' ? await this.siteService.get(site) : site;
@ -327,12 +308,11 @@ export class ShopyyService {
};
}
async getAllOrders(site: any | number, params: Record<string, any> = {}, maxPages: number = 10, concurrencyLimit: number = 100): Promise<any> {
const firstPage = await this.getOrders(site, 1, 100);
async getAllOrders(site: any | number, params: ShopyyGetAllOrdersParams = {}, maxPages: number = 10, concurrencyLimit: number = 100): Promise<any> {
const firstPage = await this.getOrders(site, 1, 100, params);
const { items: firstPageItems, totalPages} = firstPage;
const { items: firstPageItems, totalPages } = firstPage;
// const { page = 1, per_page = 100 } = params;
// 如果只有一页数据,直接返回
if (totalPages <= 1) {
return firstPageItems;
@ -353,7 +333,7 @@ export class ShopyyService {
// 创建当前批次的并发请求
for (let i = 0; i < batchSize; i++) {
const page = currentPage + i;
const pagePromise = this.getOrders(site, page, 100)
const pagePromise = this.getOrders(site, page, 100, params)
.then(pageResult => pageResult.items)
.catch(error => {
console.error(`获取第 ${page} 页数据失败:`, error);
@ -385,7 +365,7 @@ export class ShopyyService {
* @param orderId ID
* @returns
*/
async getOrder(siteId: string, orderId: string): Promise<any> {
async getOrder(siteId: string, orderId: string): Promise<ShopyyGetOneOrderResult> {
const site = await this.siteService.get(Number(siteId));
// ShopYY API: GET /orders/{id}
@ -495,13 +475,16 @@ export class ShopyyService {
async createFulfillment(site: Site, orderId: string, data: any): Promise<any> {
// ShopYY API: POST /orders/{id}/shipments
const fulfillmentData = {
data: [{
order_number: orderId,
tracking_company: data.tracking_company,
tracking_number: data.tracking_number,
carrier_code: data.carrier_code,
carrier_name: data.carrier_name,
shipping_method: data.shipping_method
note: "note",
mode: ""
}]
};
const response = await this.request(site, `orders/${orderId}/shipments`, 'POST', fulfillmentData);
const response = await this.request(site, `orders/fulfillments`, 'POST', fulfillmentData);
return response.data;
}
@ -514,7 +497,7 @@ export class ShopyyService {
*/
async deleteFulfillment(site: any, orderId: string, fulfillmentId: string): Promise<boolean> {
try {
// ShopYY API: DELETE /orders/{order_id}/shipments/{fulfillment_id}
// ShopYY API: DELETE /orders/fulfillments/{fulfillment_id}
await this.request(site, `orders/${orderId}/fulfillments/${fulfillmentId}`, 'DELETE');
return true;
} catch (error) {
@ -667,7 +650,7 @@ export class ShopyyService {
const result = response.data;
// 转换 ShopYY 批量操作结果为统一格式
const errors: Array<{identifier: string, error: string}> = [];
const errors: Array<{ identifier: string, error: string }> = [];
// 假设 ShopYY 返回格式与 WooCommerce 类似: { create: [...], update: [...], delete: [...] }
// 错误信息可能在每个项目的 error 字段中

View File

@ -1,4 +1,4 @@
import { Inject, Provide } from '@midwayjs/core';
import { ILogger, Inject, Provide } from '@midwayjs/core';
import { ShopyyAdapter } from '../adapter/shopyy.adapter';
import { WooCommerceAdapter } from '../adapter/woocommerce.adapter';
import { ISiteAdapter } from '../interface/site-adapter.interface';
@ -7,6 +7,7 @@ import { SiteService } from './site.service';
import { WPService } from './wp.service';
import { ProductService } from './product.service';
import { UnifiedProductDTO } from '../dto/site-api.dto';
import { Product } from '../entity/product.entity';
@Provide()
export class SiteApiService {
@ -22,6 +23,9 @@ export class SiteApiService {
@Inject()
productService: ProductService;
@Inject()
logger: ILogger;
async getAdapter(siteId: number): Promise<ISiteAdapter> {
const site = await this.siteService.get(siteId, true);
if (!site) {
@ -49,7 +53,7 @@ export class SiteApiService {
* @param siteProduct
* @returns ERP产品信息的站点商品
*/
async enrichSiteProductWithErpInfo(siteId: number, siteProduct: any): Promise<any> {
async enrichSiteProductWithErpInfo(siteId: number, siteProduct: UnifiedProductDTO): Promise<UnifiedProductDTO & { erpProduct?: Product }> {
if (!siteProduct || !siteProduct.sku) {
return siteProduct;
}
@ -61,18 +65,7 @@ export class SiteApiService {
// 将ERP产品信息合并到站点商品中
return {
...siteProduct,
erpProduct: {
id: erpProduct.id,
sku: erpProduct.sku,
name: erpProduct.name,
nameCn: erpProduct.nameCn,
category: erpProduct.category,
attributes: erpProduct.attributes,
components: erpProduct.components,
price: erpProduct.price,
promotionPrice: erpProduct.promotionPrice,
// 可以根据需要添加更多ERP产品字段
}
erpProduct,
};
} catch (error) {
// 如果找不到对应的ERP产品返回原始站点商品
@ -87,7 +80,7 @@ export class SiteApiService {
* @param siteProducts
* @returns ERP产品信息的站点商品列表
*/
async enrichSiteProductsWithErpInfo(siteId: number, siteProducts: any[]): Promise<any[]> {
async enrichSiteProductsWithErpInfo(siteId: number, siteProducts: UnifiedProductDTO[]): Promise<(UnifiedProductDTO & { erpProduct?: Product })[]> {
if (!siteProducts || !siteProducts.length) {
return siteProducts;
}
@ -110,36 +103,25 @@ export class SiteApiService {
const adapter = await this.getAdapter(siteId);
// 首先尝试查找产品
if (product.id) {
try {
// 尝试获取产品以确认它是否存在
const existingProduct = await adapter.getProduct(product.id);
if (existingProduct) {
// 产品存在,执行更新
return await adapter.updateProduct(product.id, product);
if (!product.sku) {
throw new Error('产品SKU不能为空');
}
} catch (error) {
// 如果获取产品失败,可能是因为产品不存在,继续执行创建逻辑
console.log(`产品 ${product.id} 不存在,将创建新产品:`, error.message);
}
} else if (product.sku) {
// 如果没有提供ID但提供了SKU尝试通过SKU查找产品
try {
// 尝试搜索具有相同SKU的产品
const searchResult = await adapter.getProducts({ where: { sku: product.sku } });
if (searchResult.items && searchResult.items.length > 0) {
const existingProduct = searchResult.items[0];
// 找到现有产品,更新它
return await adapter.updateProduct(existingProduct.id, product);
}
} catch (error) {
// 搜索失败,继续执行创建逻辑
console.log(`通过SKU搜索产品失败:`, error.message);
}
}
let existingProduct
try {
existingProduct = await adapter.getProduct({ sku: product.sku });
} catch (error) {
this.logger.error(`[Site API] 查找产品失败, siteId: ${siteId}, sku: ${product.sku}, 错误信息: ${error.message}`);
existingProduct = null
}
if (existingProduct) {
// 找到现有产品,更新它
return await adapter.updateProduct({ id: existingProduct.id }, product);
}
// 产品不存在,执行创建
return await adapter.createProduct(product);
}
/**
@ -160,7 +142,7 @@ export class SiteApiService {
const result = await this.upsertProduct(siteId, product);
// 判断是创建还是更新
if (result && result.id) {
// 简单判断如果产品原本没有ID而现在有了说明是创建的
// 简单判断:如果产品原本没有ID而现在有了说明是创建的
if (!product.id || !product.id.toString().trim()) {
results.created.push(result);
} else {
@ -189,17 +171,6 @@ export class SiteApiService {
return await adapter.getProducts(params);
}
/**
*
* @param siteId ID
* @param productId ID
* @returns
*/
async getProductFromSite(siteId: number, productId: string | number): Promise<any> {
const adapter = await this.getAdapter(siteId);
return await adapter.getProduct(productId);
}
/**
*
* @param siteId ID

View File

View File

@ -15,8 +15,19 @@ export class StatisticsService {
orderItemRepository: Repository<OrderItem>;
async getOrderStatistics(params: OrderStatisticsParams) {
const { startDate, endDate, grouping, siteId } = params;
const { startDate, endDate, grouping, siteId, country } = params;
// const keywords = keyword ? keyword.split(' ').filter(Boolean) : [];
let siteIds = []
if (country) {
siteIds = await this.getSiteIds(country)
}
if (siteId) {
siteIds.push(siteId)
}
const start = dayjs(startDate).format('YYYY-MM-DD');
const end = dayjs(endDate).add(1, 'd').format('YYYY-MM-DD');
let sql
@ -54,22 +65,24 @@ export class StatisticsService {
AND o.status IN('processing','completed')
`;
if (siteId) sql += ` AND o.siteId=${siteId}`;
if (siteIds.length) sql += ` AND o.siteId IN (${siteIds.join(',')})`;
sql += `
GROUP BY o.id, o.date_paid, o.customer_email, o.total, o.source_type, o.siteId, o.utm_source
),
order_sales_summary AS (
SELECT
orderId,
SUM(CASE WHEN name LIKE '%zyn%' THEN quantity ELSE 0 END) AS zyn_quantity,
SUM(CASE WHEN name LIKE '%yoone%' THEN quantity ELSE 0 END) AS yoone_quantity,
SUM(CASE WHEN name LIKE '%zex%' THEN quantity ELSE 0 END) AS zex_quantity,
SUM(CASE WHEN name LIKE '%yoone%' AND isPackage = 1 THEN quantity ELSE 0 END) AS yoone_G_quantity,
SUM(CASE WHEN name LIKE '%yoone%' AND isPackage = 0 THEN quantity ELSE 0 END) AS yoone_S_quantity,
SUM(CASE WHEN name LIKE '%yoone%' AND name LIKE '%3%' THEN quantity ELSE 0 END) AS yoone_3_quantity,
SUM(CASE WHEN name LIKE '%yoone%' AND name LIKE '%6%' THEN quantity ELSE 0 END) AS yoone_6_quantity,
SUM(CASE WHEN name LIKE '%yoone%' AND name LIKE '%9%' THEN quantity ELSE 0 END) AS yoone_9_quantity,
SUM(CASE WHEN name LIKE '%yoone%' AND name LIKE '%12%' THEN quantity ELSE 0 END) AS yoone_12_quantity,
SUM(CASE WHEN name LIKE '%yoone%' AND name LIKE '%15%' THEN quantity ELSE 0 END) AS yoone_15_quantity
SUM(CASE WHEN brand = 'zyn' THEN quantity ELSE 0 END) AS zyn_quantity,
SUM(CASE WHEN brand = 'yoone' THEN quantity ELSE 0 END) AS yoone_quantity,
SUM(CASE WHEN brand = 'zex' THEN quantity ELSE 0 END) AS zex_quantity,
SUM(CASE WHEN brand = 'yoone' AND isPackage = 1 THEN quantity ELSE 0 END) AS yoone_G_quantity,
SUM(CASE WHEN brand = 'yoone' AND isPackage = 0 THEN quantity ELSE 0 END) AS yoone_S_quantity,
SUM(CASE WHEN brand = 'yoone' AND strength = '3mg' THEN quantity ELSE 0 END) AS yoone_3_quantity,
SUM(CASE WHEN brand = 'yoone' AND strength = '6mg' THEN quantity ELSE 0 END) AS yoone_6_quantity,
SUM(CASE WHEN brand = 'yoone' AND strength = '9mg' THEN quantity ELSE 0 END) AS yoone_9_quantity,
SUM(CASE WHEN brand = 'yoone' AND strength = '12mg' THEN quantity ELSE 0 END) AS yoone_12_quantity,
SUM(CASE WHEN brand = 'yoone' AND strength = '15mg' THEN quantity ELSE 0 END) AS yoone_15_quantity
FROM order_sale
GROUP BY orderId
),
@ -247,22 +260,25 @@ export class StatisticsService {
LEFT JOIN order_item oi ON o.id = oi.orderId
WHERE o.date_paid IS NOT NULL
AND o.date_paid >= '${start}' AND o.date_paid < '${end}'
AND o.status IN ('processing','completed')
AND o.status IN ('processing','completed')`;
if (siteId) sql += ` AND o.siteId=${siteId}`;
if (siteIds.length) sql += ` AND o.siteId IN (${siteIds.join(',')})`;
sql +=`
GROUP BY o.id, o.date_paid, o.customer_email, o.total, o.source_type, o.siteId, o.utm_source
),
order_sales_summary AS (
SELECT
orderId,
SUM(CASE WHEN name LIKE '%zyn%' THEN quantity ELSE 0 END) AS zyn_quantity,
SUM(CASE WHEN name LIKE '%yoone%' THEN quantity ELSE 0 END) AS yoone_quantity,
SUM(CASE WHEN name LIKE '%zex%' THEN quantity ELSE 0 END) AS zex_quantity,
SUM(CASE WHEN name LIKE '%yoone%' AND isPackage = 1 THEN quantity ELSE 0 END) AS yoone_G_quantity,
SUM(CASE WHEN name LIKE '%yoone%' AND isPackage = 0 THEN quantity ELSE 0 END) AS yoone_S_quantity,
SUM(CASE WHEN name LIKE '%yoone%' AND name LIKE '%3%' THEN quantity ELSE 0 END) AS yoone_3_quantity,
SUM(CASE WHEN name LIKE '%yoone%' AND name LIKE '%6%' THEN quantity ELSE 0 END) AS yoone_6_quantity,
SUM(CASE WHEN name LIKE '%yoone%' AND name LIKE '%9%' THEN quantity ELSE 0 END) AS yoone_9_quantity,
SUM(CASE WHEN name LIKE '%yoone%' AND name LIKE '%12%' THEN quantity ELSE 0 END) AS yoone_12_quantity,
SUM(CASE WHEN name LIKE '%yoone%' AND name LIKE '%15%' THEN quantity ELSE 0 END) AS yoone_15_quantity
SUM(CASE WHEN brand = 'zyn' THEN quantity ELSE 0 END) AS zyn_quantity,
SUM(CASE WHEN brand = 'yoone' THEN quantity ELSE 0 END) AS yoone_quantity,
SUM(CASE WHEN brand = 'zex' THEN quantity ELSE 0 END) AS zex_quantity,
SUM(CASE WHEN brand = 'yoone' AND isPackage = 1 THEN quantity ELSE 0 END) AS yoone_G_quantity,
SUM(CASE WHEN brand = 'yoone' AND isPackage = 0 THEN quantity ELSE 0 END) AS yoone_S_quantity,
SUM(CASE WHEN brand = 'yoone' AND strength = '3mg' THEN quantity ELSE 0 END) AS yoone_3_quantity,
SUM(CASE WHEN brand = 'yoone' AND strength = '6mg' THEN quantity ELSE 0 END) AS yoone_6_quantity,
SUM(CASE WHEN brand = 'yoone' AND strength = '9mg' THEN quantity ELSE 0 END) AS yoone_9_quantity,
SUM(CASE WHEN brand = 'yoone' AND strength = '12mg' THEN quantity ELSE 0 END) AS yoone_12_quantity,
SUM(CASE WHEN brand = 'yoone' AND strength = '15mg' THEN quantity ELSE 0 END) AS yoone_15_quantity
FROM order_sale
GROUP BY orderId
),
@ -440,22 +456,26 @@ export class StatisticsService {
LEFT JOIN order_item oi ON o.id = oi.orderId
WHERE o.date_paid IS NOT NULL
AND o.date_paid >= '${start}' AND o.date_paid < '${end}'
`;
if (siteId) sql += ` AND o.siteId=${siteId}`;
if (siteIds.length) sql += ` AND o.siteId IN (${siteIds.join(',')})`;
sql +=`
AND o.status IN ('processing','completed')
GROUP BY o.id, o.date_paid, o.customer_email, o.total, o.source_type, o.siteId, o.utm_source
),
order_sales_summary AS (
SELECT
orderId,
SUM(CASE WHEN name LIKE '%zyn%' THEN quantity ELSE 0 END) AS zyn_quantity,
SUM(CASE WHEN name LIKE '%yoone%' THEN quantity ELSE 0 END) AS yoone_quantity,
SUM(CASE WHEN name LIKE '%zex%' THEN quantity ELSE 0 END) AS zex_quantity,
SUM(CASE WHEN name LIKE '%yoone%' AND isPackage = 1 THEN quantity ELSE 0 END) AS yoone_G_quantity,
SUM(CASE WHEN name LIKE '%yoone%' AND isPackage = 0 THEN quantity ELSE 0 END) AS yoone_S_quantity,
SUM(CASE WHEN name LIKE '%yoone%' AND name LIKE '%3%' THEN quantity ELSE 0 END) AS yoone_3_quantity,
SUM(CASE WHEN name LIKE '%yoone%' AND name LIKE '%6%' THEN quantity ELSE 0 END) AS yoone_6_quantity,
SUM(CASE WHEN name LIKE '%yoone%' AND name LIKE '%9%' THEN quantity ELSE 0 END) AS yoone_9_quantity,
SUM(CASE WHEN name LIKE '%yoone%' AND name LIKE '%12%' THEN quantity ELSE 0 END) AS yoone_12_quantity,
SUM(CASE WHEN name LIKE '%yoone%' AND name LIKE '%15%' THEN quantity ELSE 0 END) AS yoone_15_quantity
SUM(CASE WHEN brand = 'zyn' THEN quantity ELSE 0 END) AS zyn_quantity,
SUM(CASE WHEN brand = 'yoone' THEN quantity ELSE 0 END) AS yoone_quantity,
SUM(CASE WHEN brand = 'zex' THEN quantity ELSE 0 END) AS zex_quantity,
SUM(CASE WHEN brand = 'yoone' AND isPackage = 1 THEN quantity ELSE 0 END) AS yoone_G_quantity,
SUM(CASE WHEN brand = 'yoone' AND isPackage = 0 THEN quantity ELSE 0 END) AS yoone_S_quantity,
SUM(CASE WHEN brand = 'yoone' AND strength = '3mg' THEN quantity ELSE 0 END) AS yoone_3_quantity,
SUM(CASE WHEN brand = 'yoone' AND strength = '6mg' THEN quantity ELSE 0 END) AS yoone_6_quantity,
SUM(CASE WHEN brand = 'yoone' AND strength = '9mg' THEN quantity ELSE 0 END) AS yoone_9_quantity,
SUM(CASE WHEN brand = 'yoone' AND strength = '12mg' THEN quantity ELSE 0 END) AS yoone_12_quantity,
SUM(CASE WHEN brand = 'yoone' AND strength = '15mg' THEN quantity ELSE 0 END) AS yoone_15_quantity
FROM order_sale
GROUP BY orderId
),
@ -1314,7 +1334,14 @@ export class StatisticsService {
}
async getOrderSorce(params) {
const sql = `
const { country } = params;
let siteIds = []
if (country) {
siteIds = await this.getSiteIds(country)
}
let sql = `
WITH cutoff_months AS (
SELECT
DATE_FORMAT(DATE_SUB(CURDATE(), INTERVAL 7 MONTH), '%Y-%m') AS start_month,
@ -1326,7 +1353,10 @@ export class StatisticsService {
DATE_FORMAT(MIN(date_paid), '%Y-%m') AS first_order_month,
SUM(total) AS first_order_total
FROM \`order\`
WHERE status IN ('processing', 'completed')
WHERE status IN ('processing', 'completed')`;
if (siteIds.length!=0) sql += ` AND siteId IN ('${siteIds.join("','")}')`;
else sql += ` AND siteId IS NULL `;
sql += `
GROUP BY customer_email
),
order_months AS (
@ -1334,7 +1364,10 @@ export class StatisticsService {
customer_email,
DATE_FORMAT(date_paid, '%Y-%m') AS order_month
FROM \`order\`
WHERE status IN ('processing', 'completed')
WHERE status IN ('processing', 'completed')`;
if (siteIds.length!=0) sql += ` AND siteId IN ('${siteIds.join("','")}')`;
else sql += ` AND siteId IS NULL `;
sql += `
),
filtered_orders AS (
SELECT o.customer_email, o.order_month, u.first_order_month,u.first_order_total, c.start_month
@ -1366,7 +1399,7 @@ export class StatisticsService {
ORDER BY order_month DESC, first_order_month_group
`
const inactiveSql = `
let inactiveSql = `
WITH
cutoff_months AS (
SELECT
@ -1381,7 +1414,10 @@ export class StatisticsService {
date_paid,
total
FROM \`order\`
WHERE status IN ('processing', 'completed')
WHERE status IN ('processing', 'completed')`;
if (siteIds.length!=0) inactiveSql += ` AND siteId IN ('${siteIds.join("','")}')`;
else inactiveSql += ` AND siteId IS NULL `;
inactiveSql += `
),
filtered_users AS (
@ -1524,4 +1560,13 @@ export class StatisticsService {
}
async getSiteIds(country: any[]) {
const sql = `
SELECT DISTINCT sa.siteId as site_id FROM area a left join site_areas_area sa on a.id = sa.areaId WHERE a.code IN ('${country.join("','")}')
`
const res = await this.orderRepository.query(sql)
return res.map(item => item.site_id)
}
}

View File

@ -1,6 +1,8 @@
/**
*
* wp
* https://developer.wordpress.org/rest-api/reference/media/
* woocommerce
*
*/
import { Inject, Provide } from '@midwayjs/core';
import axios, { AxiosRequestConfig } from 'axios';
@ -10,7 +12,7 @@ import { IPlatformService } from '../interface/platform.interface';
import { BatchOperationDTO, BatchOperationResultDTO } from '../dto/batch.dto';
import * as FormData from 'form-data';
import * as fs from 'fs';
import { WooProduct, WooVariation } from '../dto/woocommerce.dto';
import { WooProduct, WooVariation, WpMediaGetListParams } from '../dto/woocommerce.dto';
const MAX_PAGE_SIZE = 100;
@Provide()
export class WPService implements IPlatformService {
@ -44,7 +46,7 @@ export class WPService implements IPlatformService {
* @param site
* @param namespace API , wc/v3; wcs/v1
*/
private createApi(site: any, namespace: WooCommerceRestApiVersion = 'wc/v3') {
public createApi(site: any, namespace: WooCommerceRestApiVersion = 'wc/v3') {
return new WooCommerceRestApi({
url: site.apiUrl,
consumerKey: site.consumerKey,
@ -240,9 +242,11 @@ export class WPService implements IPlatformService {
return allData;
}
async getProducts(site: any, page: number = 1, pageSize: number = 100): Promise<any> {
async getProducts(site: any, params: Record<string, any> = {}): Promise<any> {
const api = this.createApi(site, 'wc/v3');
return await this.sdkGetPage<WooProduct>(api, 'products', { page, per_page: pageSize });
const page = params.page ?? 1;
const per_page = params.per_page ?? params.pageSize ?? 100;
return await this.sdkGetPage<WooProduct>(api, 'products', { ...params, page, per_page });
}
async getProduct(site: any, id: number): Promise<any> {
@ -252,9 +256,9 @@ export class WPService implements IPlatformService {
}
// 导出 WooCommerce 产品为特殊CSV(平台特性)
// 导出 WooCommerce 产品为特殊CSV(平台特性)
async exportProductsCsvSpecial(site: any, page: number = 1, pageSize: number = 100): Promise<string> {
const list = await this.getProducts(site, page, pageSize);
const list = await this.getProducts(site, { page, per_page: pageSize });
const header = ['id','name','type','status','sku','regular_price','sale_price','stock_status','stock_quantity'];
const rows = (list.items || []).map((p: any) => [p.id,p.name,p.type,p.status,p.sku,p.regular_price,p.sale_price,p.stock_status,p.stock_quantity]);
const csv = [header.join(','), ...rows.map(r => r.map(v => String(v ?? '')).join(','))].join('\n');
@ -1042,20 +1046,7 @@ export class WPService implements IPlatformService {
};
}
public async fetchMediaPaged(site: any, params: Record<string, any> = {}) {
const page = Number(params.page ?? 1);
const per_page = Number( params.per_page ?? 20);
const where = params.where && typeof params.where === 'object' ? params.where : {};
let orderby: string | undefined = params.orderby;
let order: 'asc' | 'desc' | undefined = params.orderDir as any;
if (!orderby && params.order && typeof params.order === 'object') {
const entries = Object.entries(params.order as Record<string, any>);
if (entries.length > 0) {
const [field, dir] = entries[0];
orderby = field;
order = String(dir).toLowerCase() === 'desc' ? 'desc' : 'asc';
}
}
public async fetchMediaPaged(site: any, params: Partial<WpMediaGetListParams> = {}) {
const apiUrl = site.apiUrl;
const { consumerKey, consumerSecret } = site as any;
const endpoint = 'wp/v2/media';
@ -1064,17 +1055,21 @@ export class WPService implements IPlatformService {
const response = await axios.get(url, {
headers: { Authorization: `Basic ${auth}` },
params: {
...where,
...(params.search ? { search: params.search } : {}),
...(orderby ? { orderby } : {}),
...(order ? { order } : {}),
page,
per_page
...params,
page: params.page ?? 1,
per_page: params.per_page ?? 20,
}
});
// 检查是否有错误信息
if(response?.data?.message){
throw new Error(`获取${apiUrl}条媒体文件失败,原因为${response.data.message}`)
}
if(!Array.isArray(response.data)) {
throw new Error(`获取${apiUrl}条媒体文件失败,原因为返回数据不是数组`);
}
const total = Number(response.headers['x-wp-total'] || 0);
const totalPages = Number(response.headers['x-wp-totalpages'] || 0);
return { items: response.data, total, totalPages, page, per_page, page_size: per_page };
return { items: response.data, total, totalPages, page:params.page ?? 1, per_page: params.per_page ?? 20, page_size: params.per_page ?? 20 };
}
/**
*

View File

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

View File

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

View File

View File

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

11
src/utils/trans.util.ts Normal file
View File

@ -0,0 +1,11 @@
export const toArray = (value: any): any[] => {
if (Array.isArray(value)) return value;
if (value === undefined || value === null) return [];
return String(value).split(',').map(v => v.trim()).filter(Boolean);
};
export const toNumber = (value: any): number | undefined => {
if (value === undefined || value === null || value === '') return undefined;
const n = Number(value);
return Number.isFinite(n) ? n : undefined;
};

26
test-freightwaves.js Normal file
View File

@ -0,0 +1,26 @@
// Test script for FreightwavesService createOrder method
const { FreightwavesService } = require('./dist/service/freightwaves.service');
async function testFreightwavesService() {
try {
// Create an instance of the FreightwavesService
const service = new FreightwavesService();
// Call the test method
console.log('Starting test for createOrder method...');
const result = await service.testQueryOrder();
console.log('Test completed successfully!');
console.log('Result:', result);
console.log('\nTo run the actual createOrder request:');
console.log('1. Uncomment the createOrder call in the testCreateOrder method');
console.log('2. Update the test-secret, test-partner-id with real credentials');
console.log('3. Run this script again');
} catch (error) {
console.error('Test failed:', error);
}
}
// Run the test
testFreightwavesService();

View File

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