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

Compare commits

...

72 Commits

Author SHA1 Message Date
tikkhun ee68923f26 fix(service): 修改客户服务中的默认排序顺序
将默认排序从 orders ASC 改为 orders DESC 以符合业务需求
2026-01-30 08:49:03 +00:00
zhuotianyuan 9558761d17 fix: 修正物流别名实体文件名拼写错误并迁移文件内容
将文件名从 logistics_alias.emtity.ts 修正为 logistics_alias.entity.ts
更新所有引用该文件的导入路径
2026-01-30 15:06:44 +08:00
zhuotianyuan e30cce45b7 style: 修复代码格式问题,包括空格和缩进
调整多个服务文件中的代码格式,统一空格和缩进规范
2026-01-30 15:02:35 +08:00
zhuotianyuan 4bde698625 feat(物流): 添加物流别名映射功能并优化物流公司处理
- 新增 logistics_alias 实体用于存储物流公司别名映射
- 修改 ShipmentBookDTO 中 courierCompany 的校验规则为 any 类型
- 在订单服务中实现物流别名查询和映射功能
- 移除物流服务中 courierCompany 的特殊处理逻辑
2026-01-30 14:29:56 +08:00
zhuotianyuan 5488e1b7c6 fix(logistics): 修复货运平台courierCompany字段处理逻辑
当courierCompany为"最优物流"时设置为空字符串,否则使用原值
2026-01-30 14:29:56 +08:00
zhuotianyuan 5fa5ed21b0 feat(service): 新增Wintopay物流服务并优化订单导出和物流处理
新增Wintopay物流服务接口,支持物流信息更新功能
优化订单导出功能,增加动态商品列显示
简化物流服务状态判断逻辑,修复运单号生成问题
2026-01-30 14:29:56 +08:00
tikkhun fe30fabf08 refactor(product): 重构获取组件详情逻辑并支持数量参数
将获取组件详情的逻辑从order.service.ts移到product.service.ts中统一处理
新增quantity参数支持组件数量计算
返回结果中增加parentProduct信息用于追踪父产品
2026-01-30 14:29:56 +08:00
tikkhun 12ebad6570 fix: 修复订单服务中产品详情检查逻辑
添加对productDetail.product的检查,避免在product为undefined时访问components属性
2026-01-30 14:29:09 +08:00
tikkhun 0dac006116 feat(产品服务): 重构产品查询逻辑并添加价格字段
重构 getProductBySiteSku 方法以支持更灵活的查询条件
在 site-product 实体中添加 price 字段
新增 site-product 控制器和服务用于管理站点商品
修改订单服务以支持站点参数传递
2026-01-30 14:29:09 +08:00
tikkhun 2cc434bb19 fix: 修复客户查询条件并优化订单客户信息更新逻辑
修复客户服务中phone查询条件的可选链操作符问题
将订单服务中的客户信息更新逻辑提前到订单检查之前
2026-01-30 10:27:40 +08:00
tikkhun 6b782a9d6e refactor(service): 将 console.log 替换为 logger 并调整日志级别
使用 logger 替代 console.log 以统一日志管理
将批量导入结果的日志级别从 debug 调整为 info
2026-01-29 17:57:07 +08:00
tikkhun e94805c640 refactor: 移除调试日志并优化查询条件
清理产品变体映射中的调试日志
优化产品查询中的条件语法
2026-01-29 15:41:48 +08:00
tikkhun d4b267106e fix(订单同步): 改进错误处理和日志记录顺序
在订单控制器中增强错误信息显示,包含具体错误消息
调整订单服务中日志记录的顺序,使其在循环前执行
2026-01-29 15:16:25 +08:00
tikkhun 0b211628f3 fix(订单服务): 修复产品分类可能为空的错误并优化产品查询
修复订单服务中产品分类可能为空导致的错误,改为可选链操作
优化产品服务中组件查询逻辑,使用sku查询并添加关联关系
移除产品服务中不必要的组件查询逻辑
2026-01-29 10:27:22 +08:00
tikkhun f37de5ac32 refactor(订单服务): 移除getComponentDetailFromSiteSku中不必要的quantity参数
简化组件详情查询逻辑,默认数量设为1,不再需要外部传入quantity参数
2026-01-29 10:27:22 +08:00
tikkhun 4481cce886 fix(product): 修复产品属性重复检查逻辑并添加日志记录
修复产品属性重复检查时未考虑属性数量的问题,确保只有当属性数量完全匹配时才认为重复
添加导入结果的调试日志记录
2026-01-29 10:27:22 +08:00
tikkhun eeea2c5663 fix: 修复父产品ID可能为undefined时的处理 2026-01-29 10:27:22 +08:00
zhuotianyuan 083337a301 fix(logistics): 修复物流订单状态检查和结果合并问题
修复 resShipmentOrder 状态检查时的可选链操作问题
修正 partnerOrderNumber 的拼接格式
调整查询结果合并方式,避免直接 push 操作
优化错误处理,使用 Error 对象抛出异常
2026-01-29 10:27:22 +08:00
zhuotianyuan cee8d7e029 fix(logistics): 修复物流服务中的错误处理和快递公司字段
修复freightwaves服务中的错误响应处理,增加错误码检查
添加courierCompany字段到物流DTO以支持不同快递公司
移除订单服务中注释掉的saveOrderSale调用
更新物流服务中使用courierCompany代替硬编码的shipCompany
2026-01-29 10:27:22 +08:00
tikkhun 0d2411c511 feat(产品服务): 重构产品查询逻辑并添加价格字段
重构 getProductBySiteSku 方法以支持更灵活的查询条件
在 site-product 实体中添加 price 字段
新增 site-product 控制器和服务用于管理站点商品
修改订单服务以支持站点参数传递

fix: 修复订单服务中产品详情检查逻辑

添加对productDetail.product的检查,避免在product为undefined时访问components属性

refactor(product): 重构获取组件详情逻辑并支持数量参数

将获取组件详情的逻辑从order.service.ts移到product.service.ts中统一处理
新增quantity参数支持组件数量计算
返回结果中增加parentProduct信息用于追踪父产品
2026-01-29 10:27:10 +08:00
zhuotianyuan 8b2aea7038 feat(订单服务): 添加导出XLSX功能并优化导入逻辑
添加exportToXlsx方法支持将数据导出为XLSX格式
重构importWintopayTable方法,增加物流信息回填功能
移除未使用的BatchOperationResult导入
2026-01-26 15:41:30 +08:00
zhuotianyuan 39401aeaa0 feat(订单): 添加CSV文件导入产品功能
在订单控制器和服务中添加导入产品功能,支持通过CSV文件批量导入产品数据
文件解析使用xlsx库自动识别文件类型并处理UTF-8编码
2026-01-26 15:41:30 +08:00
tikkhun 4e1f4e192d feat(product): 重构产品与站点SKU的关联关系
将产品与站点SKU的关联从简单字符串数组改为实体关系
添加站点SKU实体和迁移脚本
更新相关DTO和服务逻辑以支持新结构
2026-01-24 18:20:17 +08:00
tikkhun 6cb793b3ca fix(库存服务): 修正转移物品查询中产品名称字段错误
将查询中的 productName 字段改为 name 字段以正确获取产品名称
2026-01-24 10:22:14 +08:00
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
49 changed files with 4350 additions and 1219 deletions

17
package-lock.json generated
View File

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

View File

@ -21,13 +21,16 @@ import {
CreateReviewDTO,
CreateVariationDTO,
UpdateReviewDTO,
OrderPaymentStatus,
} from '../dto/site-api.dto';
import { UnifiedPaginationDTO, UnifiedSearchParamsDTO, } from '../dto/api.dto';
import { UnifiedPaginationDTO, UnifiedSearchParamsDTO, ShopyyGetAllOrdersParams } from '../dto/api.dto';
import {
ShopyyAllProductQuery,
ShopyyCustomer,
ShopyyOrder,
ShopyyOrderCreateParams,
ShopyyOrderQuery,
ShopyyOrderUpdateParams,
ShopyyProduct,
ShopyyProductQuery,
ShopyyVariant,
@ -37,6 +40,7 @@ import {
OrderStatus,
} from '../enums/base.enum';
import { BatchOperationDTO, BatchOperationResultDTO } from '../dto/batch.dto';
import dayjs = require('dayjs');
export class ShopyyAdapter implements ISiteAdapter {
shopyyFinancialStatusMap = {
'200': '待支付',
@ -227,9 +231,11 @@ export class ShopyyAdapter implements ISiteAdapter {
// ========== 订单映射方法 ==========
mapPlatformToUnifiedOrder(item: ShopyyOrder): UnifiedOrderDTO {
// console.log(item)
if (!item) throw new Error('订单数据不能为空')
// 提取账单和送货地址 如果不存在则为空对象
const billing = (item as any).billing_address || {};
const shipping = (item as any).shipping_address || {};
const billing = item.billing_address || {};
const shipping = item.shipping_address || {};
// 构建账单地址对象
const billingObj: UnifiedAddressDTO = {
@ -238,7 +244,7 @@ export class ShopyyAdapter implements ISiteAdapter {
fullname: billing.name || `${item.firstname} ${item.lastname}`.trim(),
company: billing.company || '',
email: item.customer_email || item.email || '',
phone: billing.phone || (item as any).telephone || '',
phone: billing.phone || item.telephone || '',
address_1: billing.address1 || item.payment_address || '',
address_2: billing.address2 || '',
city: billing.city || item.payment_city || '',
@ -269,6 +275,7 @@ export class ShopyyAdapter implements ISiteAdapter {
state: shipping.province || item.shipping_zone || '',
postcode: shipping.zip || item.shipping_postcode || '',
method_title: item.payment_method || '',
phone: shipping.phone || item.telephone || '',
country:
shipping.country_name ||
shipping.country_code ||
@ -307,14 +314,14 @@ export class ShopyyAdapter implements ISiteAdapter {
};
const lineItems: UnifiedOrderLineItemDTO[] = (item.products || []).map(
(p: any) => ({
id: p.id,
name: p.product_title || p.name,
product_id: p.product_id,
quantity: p.quantity,
total: String(p.price ?? ''),
sku: p.sku || p.sku_code || '',
price: String(p.price ?? ''),
(product) => ({
id: product.id,
name:product.sku_value?.[0]?.value || product.product_title || product.name,
product_id: product.product_id,
quantity: product.quantity,
total: String(product.price ?? ''),
sku: product.sku || product.sku_code || '',
price: String(product.price ?? ''),
})
);
// 货币符号
@ -337,7 +344,7 @@ export class ShopyyAdapter implements ISiteAdapter {
const status = this.shopyyOrderStatusMap[item.status ?? item.order_status] || OrderStatus.PENDING;
const finalcial_status = this.shopyyFinancialStatusMap[item.financial_status]
// 发货状态
const fulfillment_status = this.shopyyFulfillmentStatusMap[item.fulfillment_status];
const fulfillment_status = this.fulfillmentStatusMap[item.fulfillment_status];
return {
id: item.id || item.order_id,
number: item.order_number || item.order_sn,
@ -367,7 +374,6 @@ export class ShopyyAdapter implements ISiteAdapter {
date_paid: typeof item.pay_at === 'number'
? item.pay_at === 0 ? null : new Date(item.pay_at * 1000).toISOString()
: null,
refunds: [],
currency_symbol: (currencySymbols[item.currency] || '$') || '',
date_created:
@ -387,6 +393,7 @@ export class ShopyyAdapter implements ISiteAdapter {
tracking_number: f.tracking_number || '',
shipping_provider: f.tracking_company || '',
shipping_method: f.tracking_company || '',
date_created: typeof f.created_at === 'number'
? new Date(f.created_at * 1000).toISOString()
: f.created_at || '',
@ -400,11 +407,11 @@ export class ShopyyAdapter implements ISiteAdapter {
return data
}
mapCreateOrderParams(data: Partial<UnifiedOrderDTO>): any {
mapCreateOrderParams(data: Partial<UnifiedOrderDTO>): ShopyyOrderCreateParams {
return data
}
mapUpdateOrderParams(data: Partial<UnifiedOrderDTO>): any {
mapUpdateOrderParams(data: Partial<UnifiedOrderDTO>): ShopyyOrderUpdateParams {
// 构建 ShopYY 订单更新参数(仅包含传入的字段)
const params: any = {};
@ -440,7 +447,7 @@ export class ShopyyAdapter implements ISiteAdapter {
}
// 更新账单地址
params.billing_address = params.billing_address || {};
params.billing_address = params?.billing_address || {};
if (data.billing.first_name !== undefined) {
params.billing_address.first_name = data.billing.first_name;
}
@ -535,8 +542,16 @@ export class ShopyyAdapter implements ISiteAdapter {
}
async getOrder(where: { id: string | number }): Promise<UnifiedOrderDTO> {
const data = await this.shopyyService.getOrder(this.site.id, String(where.id));
return this.mapPlatformToUnifiedOrder(data);
const data = await this.getOrders({
where: {
id: where.id,
},
page: 1,
per_page: 1,
})
return data.items[0] || null
// const data = await this.shopyyService.getOrder(this.site.id, String(where.id));
// return this.mapPlatformToUnifiedOrder(data);
}
async getOrders(
@ -557,9 +572,21 @@ export class ShopyyAdapter implements ISiteAdapter {
per_page,
};
}
mapGetAllOrdersParams(params: UnifiedSearchParamsDTO) :ShopyyGetAllOrdersParams{
const pay_at_min = dayjs(params.after || '').unix().toString();
const pay_at_max = dayjs(params.before || '').unix().toString();
return {
per_page: params.per_page || 100,
pay_at_min: pay_at_min,
pay_at_max: pay_at_max,
order_field: 'pay_at',
}
}
async getAllOrders(params?: UnifiedSearchParamsDTO): Promise<UnifiedOrderDTO[]> {
const data = await this.shopyyService.getAllOrders(this.site.id, params);
const normalizedParams = this.mapGetAllOrdersParams(params);
const data = await this.shopyyService.getAllOrders(this.site.id, normalizedParams);
return data.map(this.mapPlatformToUnifiedOrder.bind(this));
}
@ -697,7 +724,7 @@ export class ShopyyAdapter implements ISiteAdapter {
name: item.name || item.title,
type: String(item.product_type ?? ''),
status: mapProductStatus(item.status),
sku: item.variant?.sku || '',
sku: item.variant?.sku || item.variant?.sku_code || '',
regular_price: String(item.variant?.price ?? ''),
sale_price: String(item.special_price ?? ''),
price: String(item.price ?? ''),
@ -969,7 +996,6 @@ export class ShopyyAdapter implements ISiteAdapter {
private async getProductBySku(sku: string): Promise<UnifiedProductDTO> {
// 使用Shopyy API的搜索功能通过sku查询产品
const response = await this.getAllProducts({ where: { sku } });
console.log('getProductBySku', response)
const product = response?.[0]
if (!product) {
throw new Error(`未找到sku为${sku}的产品`);
@ -1101,8 +1127,8 @@ export class ShopyyAdapter implements ISiteAdapter {
// 映射变体
return {
id: variant.id,
name: variant.sku || '',
sku: variant.sku || '',
name: variant.title || '',
sku: variant.sku || variant.sku_code || '',
regular_price: String(variant.price ?? ''),
sale_price: String(variant.special_price ?? ''),
price: String(variant.price ?? ''),
@ -1300,8 +1326,8 @@ export class ShopyyAdapter implements ISiteAdapter {
[180]: OrderStatus.COMPLETED, // 180 已完成(确认收货) 转为 completed
[190]: OrderStatus.CANCEL // 190 取消 转为 cancelled
}
shopyyFulfillmentStatusMap = {
// 物流状态 300 未发货310 部分发货320 已发货330(确认收货)
fulfillmentStatusMap = {
// 未发货
'300': OrderFulfillmentStatus.PENDING,
// 部分发货
@ -1312,4 +1338,23 @@ export class ShopyyAdapter implements ISiteAdapter {
'330': OrderFulfillmentStatus.CANCELLED,
// 确认发货
}
// 支付状态 200 待支付210 支付中220 部分支付230 已支付240 支付失败250 部分退款260 已退款 290 已取消;
financialStatusMap = {
// 待支付
'200': OrderPaymentStatus.PENDING,
// 支付中
'210': OrderPaymentStatus.PAYING,
// 部分支付
'220': OrderPaymentStatus.PARTIALLY_PAID,
// 已支付
'230': OrderPaymentStatus.PAID,
// 支付失败
'240': OrderPaymentStatus.FAILED,
// 部分退款
'250': OrderPaymentStatus.PARTIALLY_REFUNDED,
// 已退款
'260': OrderPaymentStatus.REFUNDED,
// 已取消
'290': OrderPaymentStatus.CANCELLED,
}
}

View File

@ -17,6 +17,7 @@ import {
UnifiedVariationPaginationDTO,
CreateReviewDTO,
UpdateReviewDTO,
FulfillmentDTO,
} from '../dto/site-api.dto';
import { UnifiedPaginationDTO, UnifiedSearchParamsDTO } from '../dto/api.dto';
import {
@ -28,10 +29,15 @@ import {
WooWebhook,
WooOrderSearchParams,
WooProductSearchParams,
WpMediaGetListParams,
WooFulfillment,
MetaDataFulfillment,
} from '../dto/woocommerce.dto';
import { Site } from '../entity/site.entity';
import { WPService } from '../service/wp.service';
import { BatchOperationDTO, BatchOperationResultDTO } from '../dto/batch.dto';
import { toArray, toNumber } from '../utils/trans.util';
import dayjs = require('dayjs');
export class WooCommerceAdapter implements ISiteAdapter {
// 构造函数接收站点配置与服务实例
@ -249,13 +255,25 @@ export class WooCommerceAdapter implements ISiteAdapter {
date_modified: item.date_modified ?? item.modified,
};
}
mapMediaSearchParams(params: UnifiedSearchParamsDTO): Partial<WpMediaGetListParams> {
const page = params.page
const per_page = Number(params.per_page ?? 20);
return {
...params.where,
page,
per_page,
// orderby,
// order,
};
}
// 媒体操作方法
async getMedia(params: UnifiedSearchParamsDTO): Promise<UnifiedPaginationDTO<UnifiedMediaDTO>> {
// 获取媒体列表并映射为统一媒体DTO集合
const { items, total, totalPages, page, per_page } = await this.wpService.fetchMediaPaged(
this.site,
params
this.mapMediaSearchParams(params)
);
return {
items: items.map(this.mapPlatformToUnifiedMedia.bind(this)),
@ -317,22 +335,11 @@ export class WooCommerceAdapter implements ISiteAdapter {
// }
const mapped: any = {
...(params.search ? { search: params.search } : {}),
// ...(orderBy ? { orderBy } : {}),
page,
per_page,
};
const toArray = (value: any): any[] => {
if (Array.isArray(value)) return value;
if (value === undefined || value === null) return [];
return String(value).split(',').map(v => v.trim()).filter(Boolean);
};
const toNumber = (value: any): number | undefined => {
if (value === undefined || value === null || value === '') return undefined;
const n = Number(value);
return Number.isFinite(n) ? n : undefined;
};
// 时间过滤参数
if (where.after ?? where.date_created_after ?? where.created_after) mapped.after = String(where.after ?? where.date_created_after ?? where.created_after);
@ -343,8 +350,7 @@ export class WooCommerceAdapter implements ISiteAdapter {
// 集合过滤参数
if (where.exclude) mapped.exclude = toArray(where.exclude);
if (where.include) mapped.include = toArray(where.include);
if (where.ids) mapped.include = toArray(where.ids);
if (where.ids || where.number || where.id || where.include) mapped.include = [...new Set([where.number, where.id, ...toArray(where.ids), ...toArray(where.include)])].filter(Boolean);
if (toNumber(where.offset) !== undefined) mapped.offset = Number(where.offset);
if (where.parent ?? where.parentId) mapped.parent = toArray(where.parent ?? where.parentId);
if (where.parent_exclude ?? where.parentExclude) mapped.parent_exclude = toArray(where.parent_exclude ?? where.parentExclude);
@ -393,16 +399,17 @@ export class WooCommerceAdapter implements ISiteAdapter {
mapPlatformToUnifiedOrder(item: WooOrder): UnifiedOrderDTO {
// 将 WooCommerce 订单数据映射为统一订单DTO
// 包含账单地址与收货地址以及创建与更新时间
// 映射物流追踪信息,将后端格式转换为前端期望的格式
const fulfillments = (item.fulfillments || []).map((track: any) => ({
tracking_number: track.tracking_number || '',
shipping_provider: track.shipping_provider || '',
shipping_method: track.shipping_method || '',
status: track.status || '',
date_created: track.date_created || '',
items: track.items || [],
}));
const metaFulfillments: MetaDataFulfillment[] = item.meta_data?.find?.(_meta => _meta.key === "_wc_shipment_tracking_items")?.value || []
const fulfillments = metaFulfillments.map((track) => {
return ({
tracking_id: track.tracking_id,
tracking_number: track.tracking_number,
tracking_product_code: track.tracking_product_code,
shipping_provider: track.tracking_provider,
date_created: dayjs(track.date_shipped).toString(),
})
});
return {
id: item.id,
@ -455,30 +462,8 @@ export class WooCommerceAdapter implements ISiteAdapter {
const requestParams = this.mapOrderSearchParams(params);
const { items, total, totalPages, page, per_page } = await this.wpService.fetchResourcePaged<any>(this.site, 'orders', requestParams);
// 并行获取所有订单的履行信息
const ordersWithFulfillments = await Promise.all(
items.map(async (order: any) => {
try {
// 获取订单的履行信息
const fulfillments = await this.getOrderFulfillments(order.id);
// 将履行信息添加到订单对象中
return {
...order,
fulfillments: fulfillments || []
};
} catch (error) {
// 如果获取履行信息失败,仍然返回订单,只是履行信息为空数组
console.error(`获取订单 ${order.id} 的履行信息失败:`, error);
return {
...order,
fulfillments: []
};
}
})
);
return {
items: ordersWithFulfillments.map(this.mapPlatformToUnifiedOrder),
items: items.map(this.mapPlatformToUnifiedOrder),
total,
totalPages,
page,
@ -528,54 +513,25 @@ export class WooCommerceAdapter implements ISiteAdapter {
return await this.wpService.getFulfillments(this.site, String(orderId));
}
async createOrderFulfillment(orderId: string | number, data: {
tracking_number: string;
shipping_provider: string;
shipping_method?: string;
status?: string;
date_created?: string;
items?: Array<{
order_item_id: number;
quantity: number;
}>;
}): Promise<any> {
const shipmentData: any = {
shipping_provider: data.shipping_provider,
async createOrderFulfillment(orderId: string | number, data: FulfillmentDTO): Promise<any> {
const shipmentData: Partial<WooFulfillment> = {
tracking_provider: data.shipping_provider,
tracking_number: data.tracking_number,
};
if (data.shipping_method) {
shipmentData.shipping_method = data.shipping_method;
data_sipped: data.date_created,
// items: data.items,
}
if (data.status) {
shipmentData.status = data.status;
}
if (data.date_created) {
shipmentData.date_created = data.date_created;
}
if (data.items) {
shipmentData.items = data.items;
}
const response = await this.wpService.createFulfillment(this.site, String(orderId), shipmentData);
return response.data;
}
async updateOrderFulfillment(orderId: string | number, fulfillmentId: string, data: {
tracking_number?: string;
shipping_provider?: string;
shipping_method?: string;
status?: string;
date_created?: string;
items?: Array<{
order_item_id: number;
quantity: number;
}>;
}): Promise<any> {
return await this.wpService.updateFulfillment(this.site, String(orderId), fulfillmentId, data);
async updateOrderFulfillment(orderId: string | number, fulfillmentId: string, data: FulfillmentDTO): Promise<any> {
const shipmentData: Partial<WooFulfillment> = {
tracking_provider: data.shipping_provider,
tracking_number: data.tracking_number,
data_sipped: data.date_created,
// items: data.items,
}
return await this.wpService.updateFulfillment(this.site, String(orderId), fulfillmentId, shipmentData);
}
async deleteOrderFulfillment(orderId: string | number, fulfillmentId: string): Promise<boolean> {

View File

@ -41,6 +41,8 @@ import { Category } from '../entity/category.entity';
import DictSeeder from '../db/seeds/dict.seeder';
import CategorySeeder from '../db/seeds/category.seeder';
import CategoryAttributeSeeder from '../db/seeds/category_attribute.seeder';
import { SiteSku } from '../entity/site-sku.entity';
import { logisticsAlias } from '../entity/logistics_alias.entity';
export default {
// use for cookie sign key, should change to your own and keep security
@ -50,6 +52,7 @@ export default {
entities: [
Product,
ProductStockComponent,
SiteSku,
User,
PurchaseOrder,
PurchaseOrderItem,
@ -86,6 +89,7 @@ export default {
Area,
CategoryAttribute,
Category,
logisticsAlias,
],
synchronize: true,
logging: false,

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',
port: "23306",
host: '13.212.62.127',
port: "3306",
username: 'root',
password: '12345678',
password: 'Yoone!@.2025',
database: 'inventory_v2',
synchronize: true,
logging: true,
},
},
},

View File

@ -118,8 +118,7 @@ export class MainConfiguration {
});
try {
this.logger.info('正在检查数据库是否存在...');
this.logger.info(`正在检查数据库是否存在...`+ JSON.stringify(typeormConfig));
// 初始化临时数据源
await tempDataSource.initialize();

View File

@ -40,7 +40,6 @@ export class AreaController {
}));
return successResponse(countryList, '查询成功');
} catch (error) {
console.log(error);
return errorResponse(error?.message || error);
}
}
@ -54,7 +53,6 @@ export class AreaController {
const newArea = await this.areaService.createArea(area);
return successResponse(newArea, '创建成功');
} catch (error) {
console.log(error);
return errorResponse(error?.message || error);
}
}
@ -68,7 +66,6 @@ export class AreaController {
const updatedArea = await this.areaService.updateArea(id, area);
return successResponse(updatedArea, '更新成功');
} catch (error) {
console.log(error);
return errorResponse(error?.message || error);
}
}
@ -81,7 +78,6 @@ export class AreaController {
await this.areaService.deleteArea(id);
return successResponse(null, '删除成功');
} catch (error) {
console.log(error);
return errorResponse(error?.message || error);
}
}
@ -95,7 +91,6 @@ export class AreaController {
const { list, total } = await this.areaService.getAreaList(query);
return successResponse({ list, total }, '查询成功');
} catch (error) {
console.log(error);
return errorResponse(error?.message || error);
}
}
@ -111,7 +106,6 @@ export class AreaController {
}
return successResponse(area, '查询成功');
} catch (error) {
console.log(error);
return errorResponse(error?.message || error);
}
}

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

@ -2,6 +2,7 @@ import {
Body,
Controller,
Del,
Files,
Get,
Inject,
Param,
@ -27,7 +28,6 @@ import {
} from '../dto/order.dto';
import { User } from '../decorator/user.decorator';
import { ErpOrderStatus } from '../enums/base.enum';
@Controller('/order')
export class OrderController {
@Inject()
@ -42,8 +42,7 @@ export class OrderController {
const result = await this.orderService.syncOrders(siteId, params);
return successResponse(result);
} catch (error) {
console.log(error);
return errorResponse('同步失败');
return errorResponse(`同步失败,${error?.message || '未知错误'}`);
}
}
@ -59,7 +58,6 @@ export class OrderController {
const result = await this.orderService.syncOrderById(siteId, orderId);
return successResponse(result);
} catch (error) {
console.log(error);
return errorResponse('同步失败');
}
}
@ -264,4 +262,21 @@ export class OrderController {
return errorResponse(error?.message || '导出失败');
}
}
// 导入产品(CSV 文件)
@ApiOkResponse()
@Post('/import')
async importWintopay(@Files() files: any) {
try {
// 条件判断:确保存在文件
const file = files?.[0];
if (!file) return errorResponse('未接收到上传文件');
const result = await this.orderService.importWintopayTable(file);
return successResponse(result);
} catch (error) {
return errorResponse(error?.message || error);
}
}
}

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);
@ -176,7 +201,7 @@ export class ProductController {
@Get('/site-sku/:siteSku')
async getProductBySiteSku(@Param('siteSku') siteSku: string) {
try {
const product = await this.productService.findProductBySiteSku(siteSku);
const product = await this.productService.getProductBySiteSku(siteSku);
return successResponse(product);
} catch (error) {
return errorResponse(error.message || '获取数据失败');
@ -750,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

@ -0,0 +1,90 @@
import {
Body,
Controller,
Get,
Inject,
Post,
Query,
} from '@midwayjs/core';
import { Context } from '@midwayjs/koa';
import { ILogger } from '@midwayjs/logger';
import { ApiOkResponse } from '@midwayjs/swagger';
import { SiteProductService } from '../service/site-product.service';
import { errorResponse, successResponse } from '../utils/response.util';
@Controller('/site-product')
export class SiteProductController {
@Inject()
siteProductService: SiteProductService;
@Inject()
ctx: Context;
@Inject()
logger: ILogger;
@ApiOkResponse({
description: '获取站点商品列表',
})
@Get('/list')
async getSiteProductList(
@Query('current') current: number = 1,
@Query('pageSize') pageSize: number = 10,
@Query('siteId') siteId: number,
@Query('name') name: string,
@Query('sku') sku: string
) {
try {
const data = await this.siteProductService.getSiteProductList({
current,
pageSize,
siteId,
name,
sku,
});
return successResponse(data);
} catch (error) {
this.logger.error('获取站点商品列表失败', error);
return errorResponse(error?.message || error);
}
}
@ApiOkResponse({
description: '同步站点商品',
})
@Post('/sync')
async syncSiteProducts(@Body('siteId') siteId: number) {
try {
const result = await this.siteProductService.syncSiteProducts(siteId);
return successResponse(result);
} catch (error) {
this.logger.error('同步站点商品失败', error);
return errorResponse(error?.message || error);
}
}
@ApiOkResponse({
description: '批量修改站点商品价格',
})
@Post('/batch-update-price')
async batchUpdatePrice(
@Body('siteId') siteId: number,
@Body('productIds') productIds: string[],
@Body('price') price: number
) {
try {
const affected = await this.siteProductService.batchUpdatePrice(
siteId,
productIds,
price
);
return successResponse({
affected,
message: `成功修改 ${affected} 个商品的价格`,
});
} catch (error) {
this.logger.error('批量修改站点商品价格失败', error);
return errorResponse(error?.message || error);
}
}
}

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

@ -14,6 +14,8 @@ import { SiteService } from '../service/site.service';
import { OrderService } from '../service/order.service';
import { SiteApiService } from '../service/site-api.service';
@Controller('/webhook')
export class WebhookController {
private secret = 'YOONE24kd$kjcdjflddd';
@ -182,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

@ -0,0 +1,77 @@
import { Bootstrap } from '@midwayjs/bootstrap';
import { Product } from '../../entity/product.entity';
import { SiteSku } from '../../entity/site-sku.entity';
import { InjectEntityModel } from '@midwayjs/typeorm';
import { Repository } from 'typeorm';
import { Provide } from '@midwayjs/core';
@Provide()
export class MigrateSiteSkus {
@InjectEntityModel(Product)
productModel: Repository<Product>;
@InjectEntityModel(SiteSku)
siteSkuModel: Repository<SiteSku>;
async main() {
console.log('开始迁移 siteSkus 数据...');
try {
// 获取所有产品
const products = await this.productModel.find();
console.log(`找到 ${products.length} 个产品需要检查 siteSkus 数据`);
let migratedCount = 0;
for (const product of products) {
// 检查 siteSkus 是否为字符串数组
if (Array.isArray(product.siteSkus) && product.siteSkus.length > 0) {
for (const siteSku of product.siteSkus) {
// 检查是否已存在该 SKU
const existingSiteSku = await this.siteSkuModel.findOne({
where: { sku: siteSku.sku },
});
if (!existingSiteSku) {
// 创建新的 SiteSku 实体
const siteSku = new SiteSku();
siteSku.sku = siteSku.sku;
siteSku.productId = product.id;
siteSku.isOld = true;
await this.siteSkuModel.save(siteSku);
migratedCount++;
} else if (!existingSiteSku.productId) {
// 如果已存在但未关联产品,则关联
existingSiteSku.productId = product.id;
existingSiteSku.isOld = true;
await this.siteSkuModel.save(existingSiteSku);
migratedCount++;
}
}
}
}
console.log(`成功迁移 ${migratedCount} 条 siteSku 数据`);
console.log('数据迁移完成!');
} catch (error) {
console.error('数据迁移失败:', error);
process.exit(1);
}
}
}
// 运行迁移
if (require.main === module) {
Bootstrap
.run()
.then(async (app) => {
const migrateService = app.get(MigrateSiteSkus);
await migrateService.main();
})
.catch(error => {
console.error('启动失败:', error);
process.exit(1);
});
}

View File

@ -50,6 +50,30 @@ 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=支付时间
}
/**

View File

@ -19,15 +19,28 @@ export class ShipmentBookDTO {
@ApiProperty({ type: 'number', isArray: true })
@Rule(RuleType.array<number>().default([]))
orderIds?: number[];
@ApiProperty()
@Rule(RuleType.string())
shipmentPlatform: string;
@ApiProperty()
@Rule(RuleType.any())
courierCompany: string;
}
export class ShipmentFeeBookDTO {
@ApiProperty()
shipmentPlatform: string;
@ApiProperty()
courierCompany: string;
@ApiProperty()
stockPointId: number;
@ApiProperty()
sender: string;
@ApiProperty()
startPhone: string;
startPhone: string|any;
@ApiProperty()
startPostalCode: string;
@ApiProperty()
@ -63,6 +76,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

@ -1,6 +1,7 @@
import { ApiProperty } from '@midwayjs/swagger';
import { Rule, RuleType } from '@midwayjs/validate';
import { UnifiedSearchParamsDTO } from './api.dto';
import { SiteSku } from '../entity/site-sku.entity';
/**
* DTO
@ -59,9 +60,13 @@ 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[];
siteSkus?: SiteSku['sku'][];
// 通用属性输入(通过 attributes 统一提交品牌/口味/强度/尺寸/干湿等)
// 当 type 为 'single' 时必填,当 type 为 'bundle' 时可选
@ -86,7 +91,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,9 +147,13 @@ 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[];
siteSkus?: SiteSku['sku'][];
// 商品价格
@ApiProperty({ description: '价格', example: 99.99, required: false })
@ -153,7 +165,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 })
@ -183,7 +198,6 @@ export class UpdateProductDTO {
components?: { sku: string; quantity: number }[];
}
/**
* DTO
*/
@ -218,7 +232,7 @@ export class BatchUpdateProductDTO {
@ApiProperty({ description: '站点 SKU 列表', type: 'array', required: false })
@Rule(RuleType.array().items(RuleType.string()).optional())
siteSkus?: string[];
siteSkus?: SiteSku['sku'][];
@ApiProperty({ description: '价格', example: 99.99, required: false })
@Rule(RuleType.number().optional())
@ -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

@ -3,6 +3,7 @@ import {
UnifiedPaginationDTO,
} from './api.dto';
import { Dict } from '../entity/dict.entity';
import { Product } from '../entity/product.entity';
// export class UnifiedOrderWhere{
// []
// }
@ -18,6 +19,24 @@ export enum OrderFulfillmentStatus {
// 确认发货
CONFIRMED,
}
export enum OrderPaymentStatus {
// 待支付
PENDING,
// 支付中
PAYING,
// 部分支付
PARTIALLY_PAID,
// 已支付
PAID,
// 支付失败
FAILED,
// 部分退款
PARTIALLY_REFUNDED,
// 已退款
REFUNDED,
// 已取消
CANCELLED,
}
//
export class UnifiedProductWhere {
sku?: string;
@ -288,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 {
@ -790,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 {

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

@ -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

@ -0,0 +1,34 @@
import { ApiProperty } from '@midwayjs/swagger';
import { Entity, CreateDateColumn, UpdateDateColumn, PrimaryGeneratedColumn, Column } from 'typeorm';
@Entity('logistics_alias')
export class logisticsAlias {
@PrimaryGeneratedColumn()
id: number;
@ApiProperty({ type: 'string' })
@Column()
logistics_company: string
@ApiProperty({ type: 'string' })
@Column()
logistics_alias: string
@ApiProperty({ type: 'string' })
@Column()
platform: string
// 是否可删除
@Column({ default: true, comment: '是否可删除' })
deletable: boolean;
// 创建时间
@CreateDateColumn()
createdAt: Date;
// 更新时间
@UpdateDateColumn()
updatedAt: Date;
}

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

@ -14,6 +14,7 @@ import { ApiProperty } from '@midwayjs/swagger';
import { DictItem } from './dict_item.entity';
import { ProductStockComponent } from './product_stock_component.entity';
import { Category } from './category.entity';
import { SiteSku } from './site-sku.entity';
@Entity('product')
export class Product {
@ -55,6 +56,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 })
@ -70,6 +74,10 @@ export class Product {
@JoinColumn({ name: 'categoryId' })
category: Category;
@ApiProperty({ description: '分类 ID', nullable: true, example: 1 })
@Column({ nullable: true })
categoryId?: number;
@ManyToMany(() => DictItem, dictItem => dictItem.products, {
cascade: true,
})
@ -91,9 +99,10 @@ export class Product {
@OneToMany(() => ProductStockComponent, (component) => component.product, { cascade: true })
components: ProductStockComponent[];
@ApiProperty({ description: '站点 SKU 列表', type: 'string', isArray: true })
@Column({ type: 'simple-array' ,nullable:true})
siteSkus: string[];
// 站点 SKU 关联
@ApiProperty({ description: '站点 SKU关联', type: SiteSku, isArray: true })
@OneToMany(() => SiteSku, siteSku => siteSku.product, { cascade: true })
siteSkus: SiteSku[];
// 来源
@ApiProperty({ description: '来源', example: '1' })

View File

@ -1,7 +1,9 @@
import { ApiProperty } from '@midwayjs/swagger';
import { Column, CreateDateColumn, Entity, ManyToOne, PrimaryGeneratedColumn, UpdateDateColumn } from 'typeorm';
import { Product } from './product.entity';
// 套餐产品构成 item(sku,0013,xx20,mixed008)n--siteSkus(0013,xx20,xx)旧版数据-->product-component(0020)(套装1 5 10 20 50 100 (erp 手动创建)mixed)->(product(单品)+ quantity)->sales
// CA -> site-product -> components
// PRODUCT->compoentn一次性做完 product+关联sku
@Entity('product_stock_component')
export class ProductStockComponent {
@ApiProperty({ type: Number })
@ -11,15 +13,15 @@ export class ProductStockComponent {
@ApiProperty({ type: Number })
@Column()
productId: number;
// zi
@ApiProperty({ description: '组件所关联的 SKU', type: 'string' })
@Column({ type: 'varchar', length: 64 })
sku: string;
// zi
@ApiProperty({ type: Number, description: '组成数量' })
@Column({ type: 'int', default: 1 })
quantity: number;
// baba
// 多对一,组件隶属于一个产品
@ManyToOne(() => Product, (product) => product.components, { onDelete: 'CASCADE' })
product: Product;

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,90 @@
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: 'simple' })
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({ description: '价格' })
@Column({ type: 'decimal', precision: 10, scale: 2, nullable: true })
price: number;
@ApiProperty({
example: '2022-12-12 11:11:11',
description: '更新时间',
required: true,
})
@UpdateDateColumn()
updatedAt: Date;
}

View File

@ -0,0 +1,27 @@
import {
Column,
Entity,
ManyToOne,
JoinColumn,
} from 'typeorm';
import { ApiProperty } from '@midwayjs/swagger';
import { Product } from './product.entity';
// 这个其实是 alias 后面改一下
@Entity('product_site_sku')
export class SiteSku {
@ApiProperty({ description: 'sku'})
@Column({ primary: true })
sku: string;
@ApiProperty({ description: '商品ID' })
@Column({ nullable: true })
productId: number;
// 商品关联
@ManyToOne(() => Product, product => product.siteSkus)
@JoinColumn({ name: 'productId' })
product: Product;
@ApiProperty({ description: '是否旧版数据' })
@Column({ default: false })
isOld: boolean;
}

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

@ -0,0 +1,96 @@
import { Inject, Provide } from '@midwayjs/core';
import axios from 'axios';
import dayjs = require('dayjs');
import utc = require('dayjs/plugin/utc');
import timezone = require('dayjs/plugin/timezone');
// 扩展dayjs功能
dayjs.extend(utc);
dayjs.extend(timezone);
// Wintopay 物流更新请求接口
interface LogisticsUpdateRequest {
trade_id: string; // 订单的流水号
track_number: string; // 物流单号
track_brand: string; // 物流公司编号
}
// Wintopay 物流更新响应接口
interface LogisticsUpdateResponse {
code: string;
message: string;
data: {
trade_id: string;
track_brand: string;
track_number: string;
time: number;
};
error: any;
request_id: string;
}
@Provide()
export class WintopayService {
@Inject() logger;
// 默认配置
private config = {
//测试环境配置,在生产环境记得换掉
apiBaseUrl: 'https://stage-merchant-api.wintopay.com',
Authorization: 'Bearer kV8w1er8dFw9p9g2kb0mer398hD8hfWk',
};
// 发送请求
private async sendRequest<T>(url: string, data: any): Promise<T> {
try {
const headers = {
'Content-Type': 'application/json',
'Authorization': this.config.Authorization,
};
// 发送请求 - 临时禁用SSL证书验证以解决UNABLE_TO_VERIFY_LEAF_SIGNATURE错误
const response = await axios.post<T>(
`${this.config.apiBaseUrl}${url}`,
data,
{
headers,
httpsAgent: new (require('https').Agent)({
rejectUnauthorized: false
})
}
);
return response.data;
} catch (error) {
this.logger.error('Wintopay API请求失败:', error);
throw error;
}
}
/**
*
* @param params
* @returns
*/
async logisticsUpdate(params: LogisticsUpdateRequest): Promise<LogisticsUpdateResponse> {
try {
this.logger.info('开始更新物流信息:', params);
const response = await this.sendRequest<LogisticsUpdateResponse>('/v1/logistics/update', params);
this.logger.info('物流更新成功:', response);
return response;
} catch (error: any) {
this.logger.error('物流更新失败:', error);
// 处理API返回的错误
if (error.response?.data) {
throw new Error(`物流更新失败: ${error.response.data.message || '未知错误'}`);
}
throw new Error(`物流更新请求失败: ${error.message || '网络错误'}`);
}
}
}

View File

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

View File

@ -283,7 +283,7 @@ export class CustomerService {
orderByClause = `ORDER BY ${orderClauses.join(', ')}`;
}
} else {
orderByClause = 'ORDER BY orders ASC, yoone_total DESC';
orderByClause = 'ORDER BY orders DESC, yoone_total DESC';
}
// 主查询
@ -373,7 +373,7 @@ export class CustomerService {
per_page = 20,
where = {},
} = params;
if (where.phone) {
if (where?.phone) {
where.phone = Like(`%${where.phone}%`);
}

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') {
@ -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'] });
}
// 创建新字典项

View File

@ -0,0 +1,344 @@
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);
if (response.code !== '00000200') {
throw new Error(response.msg);
}
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);
if (response.code !== '00000200') {
throw new Error(response.msg);
}
return response.data;
}
/**
*
* @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);
if (response.code !== '00000200') {
throw new Error(response.msg);
}
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);
if (response.code !== '00000200') {
throw new Error(response.msg);
}
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;
@ -141,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);
@ -247,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);
@ -278,7 +309,6 @@ export class LogisticsService {
console.log('同步到woocommerce失败', error);
return true;
}
return true;
} catch {
throw new Error('删除运单失败');
@ -294,11 +324,23 @@ 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);
if (resShipmentFee.status !== 'SUCCESS') {
throw new Error(resShipmentFee.ret_msg);
}
return resShipmentFee.data.totalAfterTax * 100;
} else if (data.shipmentPlatform === 'freightwaves') {
const fre_reqBody = await this.convertToFreightwavesRateTry(data);
resShipmentFee = await this.freightwavesService.rateTry(fre_reqBody);
return resShipmentFee.totalAmount * 100;
} else {
throw new Error('不支持的运单平台');
}
} catch (e) {
throw e;
}
@ -319,56 +361,36 @@ 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') {
order.orderStatus = ErpOrderStatus.COMPLETED;
} else {
throw new Error('运单生成失败');
}
const dataSource = this.dataSourceManager.getDataSource('default');
let transactionError = undefined;
let shipmentId = undefined;
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.shipOrderId;
unique_id = resShipmentOrder.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,
});
@ -376,36 +398,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);
@ -415,7 +462,7 @@ export class LogisticsService {
}
};
} catch (error) {
if (resShipmentOrder.status === 'SUCCESS') {
if (resShipmentOrder?.status === 'SUCCESS') {
await this.uniExpressService.deleteShipment(resShipmentOrder.data.tno);
}
throw new Error(`上游请求错误:${error}`);
@ -642,4 +689,203 @@ 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);
// 记录物流信息,并将订单状态转到完成,uniuni状态为SUCCESStms.freightwaves状态为00000200
if (resShipmentOrder.status !== 'SUCCESS') {
throw new Error('运单生成失败');
}
}
if (data.shipmentPlatform === 'freightwaves') {
// 根据TMS系统对接说明文档格式化参数
const reqBody: any = {
// shipCompany: 'UPSYYZ7000NEW',
shipCompany: data.courierCompany,
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 }); // 查询订单
return {
...resShipmentOrder,
...queryRes
}
}
return resShipmentOrder;
} catch (error) {
// 处理错误,例如记录日志或抛出异常
throw new Error(`物流订单处理失败: ${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中缺少
shipCompany: data.courierCompany,
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

@ -1,5 +1,6 @@
import { Inject, Logger, Provide } from '@midwayjs/core';
import { WPService } from './wp.service';
import * as xlsx from 'xlsx';
import { Order } from '../entity/order.entity';
import { In, Like, Repository } from 'typeorm';
import { InjectEntityModel, TypeORMDataSourceManager } from '@midwayjs/typeorm';
@ -32,13 +33,16 @@ import { UpdateStockDTO } from '../dto/stock.dto';
import { StockService } from './stock.service';
import { OrderItemOriginal } from '../entity/order_item_original.entity';
import { SiteApiService } from './site-api.service';
import { SyncOperationResult } from '../dto/api.dto';
import { BatchErrorItem, SyncOperationResult } from '../dto/api.dto';
import * as fs from 'fs';
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';
import { Site } from '../entity/site.entity';
import { logisticsAlias } from '../entity/logistics_alias.entity';
@Provide()
export class OrderService {
@ -51,6 +55,9 @@ export class OrderService {
@InjectEntityModel(Order)
orderModel: Repository<Order>;
@InjectEntityModel(logisticsAlias)
logisticsAliasModel: Repository<logisticsAlias>;
@InjectEntityModel(User)
userModel: Repository<User>;
@ -110,6 +117,8 @@ export class OrderService {
@Logger()
logger; // 注入 Logger 实例
@Inject()
productService: ProductService;
/**
*
@ -128,7 +137,7 @@ export class OrderService {
async syncOrders(siteId: number, params: Record<string, any> = {}): Promise<SyncOperationResult> {
// 调用 WooCommerce API 获取订单
const result = await (await this.siteApiService.getAdapter(siteId)).getAllOrders(params);
this.logger.info('开始进入循环同步订单', result.length, '个订单')
// 初始化同步结果对象
const syncResult: SyncOperationResult = {
total: result.length,
@ -138,7 +147,6 @@ export class OrderService {
updated: 0,
errors: []
};
// 遍历每个订单进行同步
for (const order of result) {
try {
@ -147,7 +155,7 @@ export class OrderService {
where: { externalOrderId: String(order.id), siteId: siteId },
});
if (!existingOrder) {
console.log("数据库中不存在",order.id, '订单状态:', order.status )
this.logger.debug("数据库中不存在", order.id, '订单状态:', order.status)
}
// 同步单个订单
await this.syncSingleOrder(siteId, order);
@ -162,6 +170,7 @@ export class OrderService {
} else {
syncResult.created++;
}
// console.log('updated', syncResult.updated, 'created:', syncResult.created)
} catch (error) {
// 记录错误但不中断整个同步过程
syncResult.errors.push({
@ -171,7 +180,7 @@ export class OrderService {
syncResult.processed++;
}
}
this.logger.debug('syncOrders result', syncResult)
this.logger.info('同步完成', syncResult.updated, 'created:', syncResult.created)
return syncResult;
}
@ -209,7 +218,7 @@ export class OrderService {
where: { externalOrderId: String(order.id), siteId: siteId },
});
if (!existingOrder) {
console.log("数据库不存在", siteId , "订单:",order.id, '订单状态:' + order.status )
this.logger.debug("数据库不存在", siteId, "订单:", order.id, '订单状态:' + order.status)
}
// 同步单个订单
await this.syncSingleOrder(siteId, order, true);
@ -278,6 +287,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 +315,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,11 +329,12 @@ 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);
@ -328,10 +343,10 @@ export class OrderService {
// 矫正数据库中的订单数据
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,
@ -340,28 +355,24 @@ export class OrderService {
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,7 +470,8 @@ 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
@ -470,6 +482,20 @@ export class OrderService {
const existingOrder = await this.orderModel.findOne({
where: { externalOrderId, siteId: siteId },
});
// 提前不然存在就不更新客户信息了
// 创建或更新客户信息
await this.customerService.upsertCustomer({
email: order.customer_email,
site_id: siteId,
origin_id: String(order.customer_id),
billing: order.billing,
shipping: order.shipping,
first_name: order?.billing?.first_name || order?.shipping?.first_name,
last_name: order?.billing?.last_name || order?.shipping?.last_name,
fullname: order?.billing?.fullname || order?.shipping?.fullname || order?.billing?.first_name + ' ' + order?.billing?.last_name,
phone: order?.billing?.phone || order?.shipping?.phone,
// tags:['fromOrder']
});
// 如果订单已存在
if (existingOrder) {
// 检查是否可以更新 ERP 状态
@ -486,20 +512,7 @@ export class OrderService {
}
// 如果订单不存在,则映射订单状态
entity.orderStatus = this.mapOrderStatus(entity.status);
// 创建或更新客户信息
await this.customerService.upsertCustomer({
email: order.customer_email,
site_id: siteId,
origin_id: String(order.customer_id),
billing: order.billing,
shipping: order.shipping,
first_name: order?.billing?.first_name || order?.shipping?.first_name,
last_name: order?.billing?.last_name || order?.shipping?.last_name,
fullname: order?.billing?.fullname || order?.shipping?.fullname,
phone: order?.billing?.phone || order?.shipping?.phone,
// tags:['fromOrder']
});
// const customer = await this.customerModel.findOne({
// where: { email: order.customer_email },
// });
@ -620,7 +633,8 @@ export class OrderService {
// 保存订单项
await this.saveOrderItem(entity);
// 为每个订单项创建对应的销售项(OrderSale)
await this.saveOrderSale(entity);
const site = await this.siteService.get(siteId);
await this.saveOrderSale(entity, site);
}
}
@ -708,7 +722,9 @@ export class OrderService {
*
* @param orderItem
*/
async saveOrderSale(orderItem: OrderItem) {
// TODO 这里存的是库存商品实际
// 所以叫做 orderInventoryItems 可能更合适
async saveOrderSale(orderItem: OrderItem, site: Site) {
const currentOrderSale = await this.orderSaleModel.find({
where: {
siteId: orderItem.siteId,
@ -719,53 +735,48 @@ 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 componentDetails = await this.productService.getComponentDetailFromSiteSku({ sku: orderItem.sku, name: orderItem.name }, orderItem.quantity, site);
if (!componentDetails?.length) {
return
}
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, {
const orderSales: OrderSale[] = componentDetails.map(({ product, parentProduct, quantity }) => {
if (!product) return null
const attrsObj = this.productService.getAttributesObject(product.attributes)
const orderSale = plainToClass(OrderSale, {
orderId: orderItem.orderId,
siteId: orderItem.siteId,
externalOrderItemId: orderItem.externalOrderItemId,
productId: baseProduct.id,
name: baseProduct.name,
quantity: comp.quantity * orderItem.quantity,
sku: comp.sku,
isPackage: orderItem.name.toLowerCase().includes('package'),
});
orderSales.push(orderSaleItem);
}
}
} else {
const orderSaleItem: OrderSale = plainToClass(OrderSale, {
orderId: orderItem.orderId,
siteId: orderItem.siteId,
externalOrderItemId: orderItem.externalOrderItemId,
externalOrderItemId: orderItem.externalOrderItemId,// 原始 itemId
parentProductId: parentProduct?.id, // 父产品 ID 用于统计套餐 如果是单品则不记录
productId: product.id,
isPackage: product.type === 'bundle',// 这里是否是套餐取决于父产品
name: product.name,
quantity: orderItem.quantity,
quantity: quantity * orderItem.quantity,
sku: product.sku,
isPackage: orderItem.name.toLowerCase().includes('package'),
// 理论上直接存 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: 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 +1245,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 +1339,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 +1437,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 +1478,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 +1500,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 +1557,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 +1589,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 +1652,12 @@ export class OrderService {
* @returns
*/
async getOrderItems({
current,
pageSize,
siteId,
startDate,
endDate,
current,
pageSize,
sku,
name,
}: QueryOrderSalesDTO) {
const nameKeywords = name ? name.split(' ').filter(Boolean) : [];
@ -2164,6 +2175,7 @@ export class OrderService {
* @param data
* @returns
*/
// TODO 与 sync 逻辑不一致 如需要请修复。
async createOrder(data: Record<string, any>) {
// 从数据中解构出需要用的属性
const { siteId, sales, total, billing, customer_email, billing_phone } = data;
@ -2446,18 +2458,18 @@ export class OrderService {
*/
// TODO
async exportOrder(ids: number[]) {
// 日期 订单号 姓名地址 邮箱 号码 订单内容 盒数 换盒数 换货内容 快递号
// 日期 订单号 姓名地址 邮箱 号码 盒数 换盒数 换货内容 快递号 商品1 数量1 商品2 数量2...
interface ExportData {
'日期': string;
'订单号': string;
'姓名地址': string;
'邮箱': string;
'号码': string;
'订单内容': string;
'盒数': number;
'换盒数': number;
'换货内容': string;
'快递号': string;
[key: string]: any; // 支持动态添加的商品和数量列
}
try {
@ -2504,6 +2516,15 @@ export class OrderService {
return acc;
}, {} as Record<number, OrderItem[]>);
// 计算最大商品数量
let maxItemsCount = 0;
orders.forEach(order => {
const items = orderItemsByOrderId[order.id] || [];
if (items.length > maxItemsCount) {
maxItemsCount = items.length;
}
});
// 构建导出数据
const exportDataList: ExportData[] = orders.map(order => {
// 获取订单的订单项
@ -2512,9 +2533,6 @@ 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 shipping = order.shipping;
const billing = order.billing;
@ -2539,18 +2557,32 @@ export class OrderService {
const exchangeBoxCount = 0;
const exchangeContent = '';
return {
// 构建基础数据对象
const baseData: ExportData = {
'日期': order.date_created?.toISOString().split('T')[0] || '',
'订单号': order.externalOrderId || '',
'姓名地址': nameAddress,
'邮箱': order.customer_email || '',
'号码': phone,
'订单内容': orderContent,
'盒数': boxCount,
'换盒数': exchangeBoxCount,
'换货内容': exchangeContent,
'快递号': trackingNumber
};
// 添加商品和数量列
items.forEach((item, index) => {
baseData[`商品${index + 1}`] = item.name;
baseData[`数量${index + 1}`] = item.quantity;
});
// 填充空值,确保所有行的列数一致
for (let i = items.length; i < maxItemsCount; i++) {
baseData[`商品${i + 1}`] = '';
baseData[`数量${i + 1}`] = '';
}
return baseData;
});
// 返回CSV字符串内容给前端
@ -2624,13 +2656,9 @@ async exportToCsv(data: any[], options: { type?: 'string' | 'buffer'; fileName?:
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 +2669,272 @@ async exportToCsv(data: any[], options: { type?: 'string' | 'buffer'; fileName?:
return csvContent;
} catch (error) {
console.error('导出CSV时出错:', error);
throw new Error(`导出CSV文件失败: ${error.message}`);
}
}
/**
* XLSX格式
* @param {any[]} data
* @param {Object} options
* @param {string} [options.type='buffer'] :'buffer' | 'string' (XLSX默认返回buffer)
* @param {string} [options.fileName] (使)
* @param {boolean} [options.writeFile=false]
* @returns {string|Buffer} type返回字符串或Buffer
*/
async exportToXlsx(data: any[], options: { type?: 'buffer' | 'string'; fileName?: string; writeFile?: boolean } = {}): Promise<string | Buffer> {
try {
// 检查数据是否为空
if (!data || data.length === 0) {
throw new Error('导出数据不能为空');
}
const { type = 'buffer', fileName, writeFile = false } = options;
// 获取表头
const headers = Object.keys(data[0]);
// 构建二维数组数据(包含表头)
const aoaData = [headers];
data.forEach(item => {
const row = headers.map(key => {
const value = item[key as keyof any];
// 处理undefined和null
if (value === undefined || value === null) {
return '';
}
// 处理日期类型
if (value instanceof Date) {
return value.toISOString();
}
return value;
});
aoaData.push(row);
});
// 创建工作簿和工作表
const workbook = xlsx.utils.book_new();
const worksheet = xlsx.utils.aoa_to_sheet(aoaData);
xlsx.utils.book_append_sheet(workbook, worksheet, 'Sheet1');
// 生成XLSX buffer
const buffer = xlsx.write(workbook, { bookType: 'xlsx', type: 'buffer' });
// 如果需要写入文件
if (writeFile && fileName) {
// 获取当前用户目录
const userHomeDir = os.homedir();
// 构建目标路径(下载目录)
const downloadsDir = path.join(userHomeDir, 'Downloads');
// 确保下载目录存在
if (!fs.existsSync(downloadsDir)) {
fs.mkdirSync(downloadsDir, { recursive: true });
}
const filePath = path.join(downloadsDir, fileName);
// 写入文件
fs.writeFileSync(filePath, buffer);
return filePath;
}
// 根据类型返回不同结果
if (type === 'string') {
return buffer.toString('base64');
}
return buffer;
} catch (error) {
throw new Error(`导出XLSX文件失败: ${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;
}
// 从 CSV 导入产品;存在则更新,不存在则创建
/**
* Wintopay
* @param file
* @returns
*/
async importWintopayTable(file: any): Promise<any> {
let updated = 0;
const errors: BatchErrorItem[] = [];
// 解析文件获取工作表
let buffer: Buffer;
if (Buffer.isBuffer(file)) {
buffer = file;
} else if (file?.data) {
if (typeof file.data === 'string') {
buffer = fs.readFileSync(file.data);
} else {
buffer = file.data;
}
} else {
throw new Error('无效的文件输入');
}
const workbook = xlsx.read(buffer, { type: 'buffer', codepage: 65001 });
const worksheet = workbook.Sheets[workbook.SheetNames[0]];
// 获取表头和数据
const jsonData = xlsx.utils.sheet_to_json(worksheet, { header: 1 });
const headers = jsonData[0] as string[];
const dataRows = jsonData.slice(1) as any[][];
// 查找各列的索引
const columnIndices = {
orderNumber: headers.indexOf('订单号'),
logisticsCompany: headers.indexOf('物流公司'),
trackingNumber: headers.indexOf('单号-单元格文本格式'),
orderCreateTime: headers.indexOf('订单创建时间'),
orderEmail: headers.indexOf('订单邮箱'),
orderSite: headers.indexOf('订单网站'),
name: headers.indexOf('姓名'),
refund: headers.indexOf('退款'),
chargeback: headers.indexOf('拒付')
};
const logisticsAliases = await this.logisticsAliasModel.find();
// 构建物流公司别名映射
const logisticsAliasMap = new Map(logisticsAliases.map(alias => [alias.logistics_alias, alias.logistics_company]));
// 遍历数据行
for (let i = 0; i < dataRows.length; i++) {
const row = dataRows[i];
const orderNumber = row[columnIndices.orderNumber];
if (!orderNumber) {
errors.push({ identifier: `${i + 2}`, error: '订单号为空' });
continue;
}
try {
let orderNumbers = orderNumber;
// 确保 orderNumber 是字符串类型
const orderNumberStr = String(orderNumber);
if (orderNumberStr.includes('_') && orderNumberStr.includes('-')) {
orderNumbers = orderNumberStr.split('_')[0].toString();
orderNumbers = orderNumbers.split('-')[1];
}
// 通过订单号查询订单
const order = await this.orderModel.findOne({ where: { externalOrderId: orderNumbers } });
if (order) {
// 通过orderId查询fulfillments
const fulfillments = await this.orderFulfillmentModel.find({ where: { order_id: order.id } });
if (fulfillments && fulfillments.length > 0) {
const fulfillment = fulfillments[0]; // 假设每个订单只有一个物流信息
const shipping_provider = logisticsAliasMap.get(fulfillment.shipping_provider);
// 回填物流信息
if (columnIndices.logisticsCompany !== -1) {
row[columnIndices.logisticsCompany] = shipping_provider || '';
}
if (columnIndices.trackingNumber !== -1) {
row[columnIndices.trackingNumber] = fulfillment.tracking_number || '';
}
updated++;
}
}
} catch (error) {
errors.push({ identifier: `${i + 2}`, error: `处理失败: ${error.message}` });
}
}
// 将数据转换为对象数组,与 exportOrder 方法返回格式一致
const resultData = dataRows.map((row, index) => {
const rowData: any = {};
headers.forEach((header, colIndex) => {
rowData[header] = row[colIndex] || '';
});
// 添加行号信息
rowData['行号'] = index + 2;
return rowData;
});
// 返回XLSX buffer内容给前端
// const xlsxBuffer = await this.exportToXlsx(resultData, { type: 'buffer' });
return resultData;
}
}

File diff suppressed because it is too large Load Diff

View File

@ -8,9 +8,9 @@ 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平台服务实现
*/
@ -288,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;
@ -308,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 { page = 1, per_page = 100 } = params;
// 如果只有一页数据,直接返回
if (totalPages <= 1) {
return firstPageItems;
@ -334,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);
@ -366,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}
@ -476,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;
}
@ -495,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) {

View File

@ -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 {
@ -52,30 +53,19 @@ 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;
}
try {
// 使用站点SKU查询对应的ERP产品
const erpProduct = await this.productService.findProductBySiteSku(siteProduct.sku);
const erpProduct = await this.productService.getProductBySiteSku(siteProduct.sku);
// 将ERP产品信息合并到站点商品中
return {
...siteProduct,
erpProduct: {
id: erpProduct.id,
sku: erpProduct.sku,
name: erpProduct.name,
nameCn: erpProduct.nameCn,
category: erpProduct.category,
attributes: erpProduct.attributes,
components: erpProduct.components,
price: erpProduct.price,
promotionPrice: erpProduct.promotionPrice,
// 可以根据需要添加更多ERP产品字段
}
erpProduct,
};
} catch (error) {
// 如果找不到对应的ERP产品返回原始站点商品
@ -90,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;
}

View File

@ -0,0 +1,102 @@
import { Inject, Provide } from '@midwayjs/core';
import { ILogger } from '@midwayjs/logger';
import { InjectEntityModel } from '@midwayjs/typeorm';
import { Repository } from 'typeorm';
import { SiteProduct } from '../entity/site-product.entity';
@Provide()
export class SiteProductService {
@InjectEntityModel(SiteProduct)
siteProductModel: Repository<SiteProduct>;
@Inject()
logger: ILogger;
async getSiteProductList(params: {
current?: number;
pageSize?: number;
siteId?: number;
name?: string;
sku?: string;
}) {
const {
current = 1,
pageSize = 10,
siteId,
name,
sku,
} = params;
const queryBuilder = this.siteProductModel.createQueryBuilder('siteProduct');
// 根据 siteId 筛选
if (siteId) {
queryBuilder.where('siteProduct.siteId = :siteId', { siteId });
}
// 根据 name 或 sku 模糊搜索
if (name || sku) {
queryBuilder.andWhere(
'(siteProduct.name LIKE :keyword OR siteProduct.sku LIKE :keyword)',
{ keyword: `%${name || sku}%` }
);
}
// 计算总数
const total = await queryBuilder.getCount();
// 分页查询
const items = await queryBuilder
.skip((current - 1) * pageSize)
.take(pageSize)
.orderBy('siteProduct.updatedAt', 'DESC')
.getMany();
return {
total,
items,
current,
pageSize,
};
}
async getSiteProductById(id: string) {
return await this.siteProductModel.findOne({ where: { id } });
}
async createSiteProduct(data: Partial<SiteProduct>) {
const siteProduct = this.siteProductModel.create(data);
return await this.siteProductModel.save(siteProduct);
}
async updateSiteProduct(id: string, data: Partial<SiteProduct>) {
await this.siteProductModel.update(id, data);
return await this.getSiteProductById(id);
}
async deleteSiteProduct(id: string) {
await this.siteProductModel.delete(id);
return true;
}
async batchUpdatePrice(siteId: number, productIds: string[], price: number) {
const result = await this.siteProductModel
.createQueryBuilder()
.update()
.set({ price })
.where('siteId = :siteId AND id IN (:...productIds)', { siteId, productIds })
.execute();
return result.affected || 0;
}
async syncSiteProducts(siteId: number) {
// 这里实现同步逻辑,暂时返回成功
// 实际实现时需要调用对应的站点适配器进行同步
this.logger.info(`Syncing products for site ${siteId}`);
return {
success: true,
message: '同步成功',
};
}
}

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

@ -547,7 +547,7 @@ export class StockService {
qb
.select([
'ti.transferId AS transferId',
"JSON_ARRAYAGG(JSON_OBJECT('id', ti.id, 'productName', ti.productName,'sku', ti.sku, 'quantity', ti.quantity)) AS items",
"JSON_ARRAYAGG(JSON_OBJECT('id', ti.id, 'productName', ti.name,'sku', ti.sku, 'quantity', ti.quantity)) AS items",
])
.from(TransferItem, 'ti')
.groupBy('ti.transferId'),

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 {
@ -1044,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';
@ -1066,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 };
}
/**
*
@ -1212,10 +1205,12 @@ export class WPService implements IPlatformService {
throw new Error('source_url 不存在');
}
// 下载源文件为 Buffer
const resp = await axios.get(srcUrl, { responseType: 'arraybuffer', timeout: 30000,
const resp = await axios.get(srcUrl, {
responseType: 'arraybuffer', timeout: 30000,
headers: {
'User-Agent': 'Mozilla/5.0 (compatible; Node.js Axios)',
} });
}
});
const inputBuffer = Buffer.from(resp.data);
// 条件判断 如果下载的 Buffer 为空则抛出错误
if (!inputBuffer || inputBuffer.length === 0) {

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();