diff --git a/src/adapter/shopyy.adapter.ts b/src/adapter/shopyy.adapter.ts index 48d1014..3c02baa 100644 --- a/src/adapter/shopyy.adapter.ts +++ b/src/adapter/shopyy.adapter.ts @@ -23,7 +23,7 @@ import { 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, @@ -40,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': '待支付', @@ -570,9 +571,21 @@ export class ShopyyAdapter implements ISiteAdapter { per_page, }; } + mapGetAllOrdersParams(params: UnifiedSearchParamsDTO) :ShopyyGetAllOrdersParams{ + const pay_at_min = dayjs(params.after || '').valueOf().toString(); + const pay_at_max = dayjs(params.before || '').valueOf().toString(); + + return { + page: params.page || 1, + per_page: params.per_page || 20, + pay_at_min: pay_at_min, + pay_at_max: pay_at_max, + } + } async getAllOrders(params?: UnifiedSearchParamsDTO): Promise { - 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)); } diff --git a/src/dto/api.dto.ts b/src/dto/api.dto.ts index 6cd4c41..bdd7d50 100644 --- a/src/dto/api.dto.ts +++ b/src/dto/api.dto.ts @@ -52,6 +52,23 @@ export class UnifiedSearchParamsDTO> { orderBy?: Record | string; } +/** + * Shopyy获取所有订单参数DTO + */ +export class ShopyyGetAllOrdersParams { + @ApiProperty({ description: '页码', example: 1, required: false }) + page?: number; + + @ApiProperty({ description: '每页数量', example: 20, 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; +} + /** * 批量操作错误项 */ diff --git a/src/service/freightwaves.service.ts b/src/service/freightwaves.service.ts new file mode 100644 index 0000000..bcd6ca5 --- /dev/null +++ b/src/service/freightwaves.service.ts @@ -0,0 +1,486 @@ +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; +} + +// 费用试算请求接口 +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 { + code: string; + msg: string; + data: T; +} + +// 费用试算响应数据接口 +interface RateTryResponseData { + shipCompany: string; + channelCode: string; + totalAmount: number; + currency: string; +} + +// 创建订单响应数据接口 +interface CreateOrderResponseData { + partnerOrderNumber: string; + shipOrderId: string; +} + +// 查询订单响应数据接口 +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: 'https://tms.freightwaves.ca', + partner: '25072621035200000060', + }; + + // 初始化配置 + setConfig(config: Partial): 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(url: string, data: any): Promise> { + 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), + }; + + // 记录请求前的详细信息 + this.log(`Sending request to: ${this.config.apiBaseUrl}${url}`, { + headers, + data + }); + + // 发送请求 - 临时禁用SSL证书验证以解决UNABLE_TO_VERIFY_LEAF_SIGNATURE错误 + const response = await axios.post>( + `${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): Promise { + const requestData: RateTryRequest = { + ...params, + partner: this.config.partner, + }; + + const response = await this.sendRequest('/shipService/order/rateTry', requestData); + return response.data; + } + + /** + * 创建订单 + * @param params 创建订单参数 + * @returns 创建订单结果 + */ + async createOrder(params: Omit): Promise { + const requestData: CreateOrderRequest = { + ...params, + partner: this.config.partner, + }; + + const response = await this.sendRequest('/shipService/order/createOrder?apipost_id=0422aa', requestData); + return response.data; + } + + /** + * 查询订单 + * @param params 查询订单参数 + * @returns 查询订单结果 + */ + async queryOrder(params: Omit): Promise { + const requestData: QueryOrderRequest = { + ...params, + partner: this.config.partner, + }; + + const response = await this.sendRequest('/shipService/order/queryOrder', requestData); + return response.data; + } + + /** + * 修改订单 + * @param params 修改订单参数 + * @returns 修改订单结果 + */ + async modifyOrder(params: Omit): Promise { + const requestData: ModifyOrderRequest = { + ...params, + partner: this.config.partner, + }; + + const response = await this.sendRequest('/shipService/order/modifyOrder', requestData); + return response.data; + } + + /** + * 订单退款 + * @param params 订单退款参数 + * @returns 订单退款结果 + */ + async refundOrder(params: Omit): Promise { + const requestData: RefundOrderRequest = { + ...params, + partner: this.config.partner, + }; + + const response = await this.sendRequest('/shipService/order/refundOrder', requestData); + return response.data; + } + + /** + * 测试创建订单方法 + * 用于演示如何使用createOrder方法 + */ + async testCreateOrder() { + try { + // 设置必要的配置 + this.setConfig({ + appSecret: 'gELCHguGmdTLo!zfihfM91hae8G@9Sz23Mh6pHrt', + apiBaseUrl: 'https://tms.freightwaves.ca', + partner: '25072621035200000060' + }); + + // 准备测试数据 + const testParams: Omit = { + shipCompany: 'DHL', + partnerOrderNumber: `test-order-${Date.now()}`, + warehouseId: '25072621035200000060', + shipper: { + name: 'John Doe', + phone: '123-456-7890', + company: 'Test Company', + countryCode: 'CA', + city: 'Toronto', + state: 'ON', + address1: '123 Main St', + address2: 'Suite 400', + postCode: 'M5V 2T6', + countryName: 'Canada', + cityName: 'Toronto', + stateName: 'Ontario', + companyName: 'Test Company Inc.' + }, + reciver: { + name: 'Jane Smith', + phone: '987-654-3210', + company: 'Receiver Company', + countryCode: 'CA', + city: 'Vancouver', + state: 'BC', + address1: '456 Oak St', + address2: '', + postCode: 'V6J 2A9', + countryName: 'Canada', + cityName: 'Vancouver', + stateName: 'British Columbia', + companyName: 'Receiver Company Ltd.' + }, + packages: [ + { + dimensions: { + length: 10, + width: 8, + height: 6, + lengthUnit: 'IN', + weight: 5, + weightUnit: 'LB' + }, + currency: 'CAD', + description: 'Test Package' + } + ], + declaration: { + boxNo: 'BOX-001', + sku: 'TEST-SKU-001', + cnname: '测试产品', + enname: 'Test Product', + declaredPrice: 100, + declaredQty: 1, + material: 'Plastic', + intendedUse: 'General use', + cweight: 5, + hsCode: '39269090', + battery: 'No' + }, + signService: 0 + }; + + // 调用创建订单方法 + this.log('开始测试创建订单...'); + this.log('测试参数:', testParams); + + // 注意:在实际环境中取消注释以下行来执行真实请求 + const result = await this.createOrder(testParams); + this.log('创建订单成功:', result); + + + // 返回模拟结果 + return { + partnerOrderNumber: testParams.partnerOrderNumber, + shipOrderId: `simulated-shipOrderId-${Date.now()}` + }; + } catch (error) { + this.log('测试创建订单失败:', error); + throw error; + } + } + + /** + * 测试查询订单方法 + * @returns 查询订单结果 + */ + async testQueryOrder() { + try { + // 设置必要的配置 + this.setConfig({ + appSecret: 'gELCHguGmdTLo!zfihfM91hae8G@9Sz23Mh6pHrt', + apiBaseUrl: 'https://console-mock.apipost.cn/mock/0', + partner: '25072621035200000060' + }); + + // 准备测试数据 - 可以通过partnerOrderNumber或shipOrderId查询 + const testParams: Omit = { + // 选择其中一个参数进行测试 + partnerOrderNumber: 'test-order-123456789', // 示例订单号 + // shipOrderId: 'simulated-shipOrderId-123456789' // 或者使用运单号 + }; + + // 调用查询订单方法 + this.log('开始测试查询订单...'); + this.log('测试参数:', testParams); + + // 注意:在实际环境中取消注释以下行来执行真实请求 + const result = await this.queryOrder(testParams); + this.log('查询订单成功:', result); + + this.log('测试完成:查询订单方法调用成功(模拟)'); + + // 返回模拟结果 + return { + thirdOrderId: 'thirdOrder-123456789', + shipCompany: 'DHL', + expressFinish: 0, + expressFailMsg: '', + expressOrder: { + mainTrackingNumber: '1234567890', + labelPath: ['https://example.com/label.pdf'], + totalAmount: 100, + currency: 'CAD', + balance: 50 + }, + partnerOrderNumber: testParams.partnerOrderNumber, + shipOrderId: 'simulated-shipOrderId-123456789' + }; + } catch (error) { + this.log('测试查询订单失败:', error); + throw error; + } + } + + /** + * 辅助日志方法,处理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); + } + } + } +} \ No newline at end of file diff --git a/src/service/order.service copy.ts b/src/service/order.service copy.ts deleted file mode 100644 index db2cccd..0000000 --- a/src/service/order.service copy.ts +++ /dev/null @@ -1,2651 +0,0 @@ -import { Inject, Logger, Provide } from '@midwayjs/core'; -import { WPService } from './wp.service'; -import { Order } from '../entity/order.entity'; -import { In, Like, Repository } from 'typeorm'; -import { InjectEntityModel, TypeORMDataSourceManager } from '@midwayjs/typeorm'; -import { plainToClass } from 'class-transformer'; -import { OrderItem } from '../entity/order_item.entity'; - -import { OrderSale } from '../entity/order_sale.entity'; -import { Product } from '../entity/product.entity'; -import { OrderFee } from '../entity/order_fee.entity'; -import { OrderRefund } from '../entity/order_refund.entity'; -import { OrderRefundItem } from '../entity/order_refund_item.entity'; -import { OrderCoupon } from '../entity/order_coupon.entity'; -import { OrderShipping } from '../entity/order_shipping.entity'; -import { OrderFulfillment } from '../entity/order_fulfillment.entity'; -import { Shipment } from '../entity/shipment.entity'; -import { Customer } from '../entity/customer.entity'; -import { - ErpOrderStatus, - OrderStatus, - StockRecordOperationType, -} from '../enums/base.enum'; -import { CreateOrderNoteDTO, QueryOrderSalesDTO } from '../dto/order.dto'; -import dayjs = require('dayjs'); -import { OrderDetailRes } from '../dto/reponse.dto'; -import { OrderNote } from '../entity/order_note.entity'; -import { User } from '../entity/user.entity'; -import { SiteService } from './site.service'; -import { ShipmentItem } from '../entity/shipment_item.entity'; -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 * 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'; -@Provide() -export class OrderService { - - @Inject() - wpService: WPService; - - @Inject() - stockService: StockService; - - @InjectEntityModel(Order) - orderModel: Repository; - - @InjectEntityModel(User) - userModel: Repository; - - @InjectEntityModel(OrderItem) - orderItemModel: Repository; - - @InjectEntityModel(OrderItem) - orderItemOriginalModel: Repository; - - @InjectEntityModel(OrderSale) - orderSaleModel: Repository; - - @InjectEntityModel(Product) - productModel: Repository; - - @InjectEntityModel(OrderFee) - orderFeeModel: Repository; - - @InjectEntityModel(OrderRefund) - orderRefundModel: Repository; - - @InjectEntityModel(OrderRefundItem) - orderRefundItemModel: Repository; - - @InjectEntityModel(OrderCoupon) - orderCouponModel: Repository; - - @InjectEntityModel(OrderShipping) - orderShippingModel: Repository; - - @InjectEntityModel(OrderFulfillment) - orderFulfillmentModel: Repository; - - @InjectEntityModel(Shipment) - shipmentModel: Repository; - - @InjectEntityModel(ShipmentItem) - shipmentItemModel: Repository; - - @InjectEntityModel(OrderNote) - orderNoteModel: Repository; - - @Inject() - dataSourceManager: TypeORMDataSourceManager; - - @InjectEntityModel(Customer) - customerModel: Repository; - - @Inject() - siteService: SiteService; - - @Inject() - siteApiService: SiteApiService; - - @Inject() - customerService: CustomerService; - - @Logger() - logger; // 注入 Logger 实例 - - /** - * 批量同步订单 - * 流程说明: - * 1. 调用 WooCommerce API 获取订单列表 - * 2. 遍历每个订单,检查数据库中是否已存在 - * 3. 调用 syncSingleOrder 同步单个订单 - * 4. 统计同步结果(创建数、更新数、错误数) - * - * 涉及实体: Order - * - * @param siteId 站点ID - * @param params 查询参数 - * @returns 同步操作结果 - */ - async syncOrders(siteId: number, params: Record = {}): Promise { - // 调用 WooCommerce API 获取订单 - const result = await (await this.siteApiService.getAdapter(siteId)).getAllOrders(params); - - // 初始化同步结果对象 - const syncResult: SyncOperationResult = { - total: result.length, - processed: 0, - synced: 0, - created: 0, - updated: 0, - errors: [] - }; - - // 遍历每个订单进行同步 - for (const order of result) { - try { - // 检查订单是否已存在,以区分创建和更新 - const existingOrder = await this.orderModel.findOne({ - where: { externalOrderId: String(order.id), siteId: siteId }, - }); - if (!existingOrder) { - console.log("数据库中不存在", order.id, '订单状态:', order.status) - } - // 同步单个订单 - await this.syncSingleOrder(siteId, order); - - // 统计结果 - syncResult.processed++; - syncResult.synced++; - - // 根据订单是否存在,更新创建或更新计数 - if (existingOrder) { - syncResult.updated++; - } else { - syncResult.created++; - } - } catch (error) { - // 记录错误但不中断整个同步过程 - syncResult.errors.push({ - identifier: String(order.id), - error: error.message || '同步失败' - }); - syncResult.processed++; - } - } - this.logger.debug('syncOrders result', syncResult) - return syncResult; - } - - /** - * 根据订单ID同步单个订单 - * 流程说明: - * 1. 调用 WooCommerce API 获取指定订单 - * 2. 检查数据库中是否已存在该订单 - * 3. 调用 syncSingleOrder 同步订单数据 - * 4. 统计同步结果 - * - * 涉及实体: Order - * - * @param siteId 站点ID - * @param orderId 订单ID - * @returns 同步操作结果 - */ - async syncOrderById(siteId: number, orderId: string): Promise { - const syncResult: SyncOperationResult = { - total: 1, - processed: 0, - synced: 0, - created: 0, - updated: 0, - errors: [] - }; - - try { - // 调用 WooCommerce API 获取订单 - const adapter = await this.siteApiService.getAdapter(siteId); - const order = await adapter.getOrder({ id: orderId }); - - // 检查订单是否已存在,以区分创建和更新 - const existingOrder = await this.orderModel.findOne({ - where: { externalOrderId: String(order.id), siteId: siteId }, - }); - if (!existingOrder) { - console.log("数据库不存在", siteId, "订单:", order.id, '订单状态:' + order.status) - } - // 同步单个订单 - await this.syncSingleOrder(siteId, order, true); - - // 统计结果 - syncResult.processed = 1; - syncResult.synced = 1; - - if (existingOrder) { - syncResult.updated = 1; - } else { - syncResult.created = 1; - } - - return syncResult; - } catch (error) { - // 记录错误 - syncResult.errors.push({ - identifier: orderId, - error: error.message || '同步失败' - }); - syncResult.processed = 1; - - return syncResult; - } - } - /** - * 订单状态自动切换映射表 - * 用于将 WordPress 订单状态转换为系统内部状态 - */ - orderAutoNextStatusMap = { - [OrderStatus.RETURN_APPROVED]: OrderStatus.ON_HOLD, // 退款申请已通过转为 on-hold - [OrderStatus.RETURN_CANCELLED]: OrderStatus.REFUNDED // 已取消退款转为 refunded - } - - /** - * 自动更新订单状态 - * 流程说明: - * 1. 检查订单状态是否需要转换 - * 2. 如果需要转换,先同步状态到 WooCommerce - * 3. 然后将订单状态切换到下一状态 - * - * 涉及实体: Order - * - * @param siteId 站点ID - * @param order 订单对象 - */ - async autoUpdateOrderStatus(siteId: number, order: any) { - // console.log('更新订单状态', order.status, '=>', this.orderAutoNextStatusMap[order.status]) - // 其他状态保持不变 - const originStatus = order.status; - // 如果有值就赋值 - if (!this.orderAutoNextStatusMap[originStatus]) { - return - } - try { - const site = await this.siteService.get(siteId); - // 仅处理 WooCommerce 站点 - if (site.type !== 'woocommerce') { - return - } - // 将订单状态同步到 WooCommerce,然后切换至下一状态 - await this.wpService.updateOrder(site, String(order.id), { status: order.status }); - order.status = this.orderAutoNextStatusMap[originStatus]; - } catch (error) { - console.error('更新订单状态失败,原因为:', error) - } - } - /** - * 同步单个订单 - * 流程说明: - * 1. 从订单数据中解构出各个子项(line_items, shipping_lines, fee_lines, coupon_lines, refunds, fulfillments) - * 2. 检查数据库中是否已存在该订单 - * 3. 自动更新订单状态(如果需要) - * 4. 如果订单状态为 AUTO_DRAFT,则跳过处理 - * 5. 如果订单从未完成变为完成状态,则更新库存并返回 - * 6. 如果订单不可编辑且不强制更新,则跳过处理 - * 7. 保存订单主数据 - * 8. 保存订单项(OrderItem) - * 9. 保存退款信息(OrderRefund, OrderRefundItem) - * 10. 保存费用信息(OrderFee) - * 11. 保存优惠券信息(OrderCoupon) - * 12. 保存配送信息(OrderShipping) - * 13. 保存履约信息(OrderFulfillment) - * - * 涉及实体: Order, OrderItem, OrderRefund, OrderRefundItem, OrderFee, OrderCoupon, OrderShipping, OrderFulfillment - * - * @param siteId 站点ID - * @param order 订单数据 - * @param forceUpdate 是否强制更新 - */ - async syncSingleOrder(siteId: number, order: any, forceUpdate = false) { - // 从订单数据中解构出各个子项 - let { - line_items, - shipping_lines, - fee_lines, - coupon_lines, - refunds, - fulfillments, // 物流信息 - ...orderData - } = order; - // console.log('同步进单个订单', order) - // 如果订单状态为 AUTO_DRAFT,则跳过处理 - if (order.status === OrderStatus.AUTO_DRAFT) { - return; - } - // 检查数据库中是否已存在该订单 - const existingOrder = await this.orderModel.findOne({ - where: { externalOrderId: order.id, siteId: siteId }, - }); - // 自动更新订单状态(如果需要) - await this.autoUpdateOrderStatus(siteId, order); - - if (existingOrder) { - // 矫正数据库中的订单数据 - const updateData: any = { status: order.status }; - if (this.canUpdateErpStatus(existingOrder.orderStatus)) { - updateData.orderStatus = this.mapOrderStatus(order.status); - } - // 更新 - await this.orderModel.update({ externalOrderId: order.id, siteId: siteId }, updateData); - // 更新 fulfillments 数据 - await this.saveOrderFulfillments({ - siteId, - orderId: existingOrder.id, - externalOrderId: order.id, - fulfillments: fulfillments, - }); - } - const externalOrderId = order.id; - // 如果订单从未完成变为完成状态,则更新库存 - if ( - existingOrder && - existingOrder.orderStatus !== ErpOrderStatus.COMPLETED && - orderData.status === OrderStatus.COMPLETED - ) { - this.updateStock(existingOrder); - // 不再直接返回,继续执行后续的更新操作 - } - // 如果订单不可编辑且不强制更新,则跳过处理 - if (existingOrder && !existingOrder.is_editable && !forceUpdate) { - return; - } - // 保存订单主数据 - const orderRecord = await this.saveOrder(siteId, orderData); - const orderId = orderRecord.id; - // 保存订单项 - await this.saveOrderItems({ - siteId, - orderId, - externalOrderId, - orderItems: line_items, - }); - // 保存退款信息 - await this.saveOrderRefunds({ - siteId, - orderId, - externalOrderId, - refunds, - }); - // 保存费用信息 - await this.saveOrderFees({ - siteId, - orderId, - externalOrderId, - fee_lines, - }); - // 保存优惠券信息 - await this.saveOrderCoupons({ - siteId, - orderId, - externalOrderId, - coupon_lines, - }); - // 保存配送信息 - await this.saveOrderShipping({ - siteId, - orderId, - externalOrderId, - shipping_lines, - }); - // 保存履约信息 - await this.saveOrderFulfillments({ - siteId, - orderId, - externalOrderId, - fulfillments: fulfillments, - }); - } - - /** - * 更新订单库存 - * 流程说明: - * 1. 查询订单的所有销售项(OrderSale) - * 2. 遍历每个销售项,创建库存更新记录 - * 3. 调用 StockService 更新库存(出库操作) - * - * 涉及实体: OrderSale, Stock - * - * @param existingOrder 已存在的订单 - */ - async updateStock(existingOrder: Order) { - const items = await this.orderSaleModel.find({ - where: { orderId: existingOrder.id }, - }); - if (!items) return; - const stockPointId = 2; - // ['YT', 'NT', 'BC', 'AB', 'SK'].some( - // v => - // v.toLowerCase() === - // ( - // existingOrder?.shipping?.state || existingOrder?.billing?.state - // ).toLowerCase() - // ) - // ? 3 - // : 2; - for (const item of items) { - const updateStock = new UpdateStockDTO(); - updateStock.stockPointId = stockPointId; - updateStock.sku = item.sku; - updateStock.quantityChange = item.quantity; - updateStock.operationType = StockRecordOperationType.OUT; - updateStock.operatorId = 1; - updateStock.note = `订单${existingOrder.externalOrderId} 出库`; - await this.stockService.updateStock(updateStock); - } - } - - /** - * 保存订单主数据 - * 流程说明: - * 1. 将外部订单ID转换为字符串 - * 2. 创建订单实体对象 - * 3. 检查数据库中是否已存在该订单 - * 4. 如果存在: - * - 检查是否可以更新 ERP 状态 - * - 如果可以,则更新订单状态并保存 - * 5. 如果不存在: - * - 映射订单状态 - * - 创建或更新客户信息 - * - 保存新订单 - * - * 涉及实体: Order, Customer - * - * @param siteId 站点ID - * @param order 订单数据 - * @returns 保存后的订单实体 - */ - async saveOrder(siteId: number, order: UnifiedOrderDTO): Promise { - // 将外部订单ID转换为字符串 - const externalOrderId = String(order.id) - delete order.id - - // 创建订单实体对象 - const entity = plainToClass(Order, { ...order, externalOrderId, siteId }); - // 检查数据库中是否已存在该订单 - const existingOrder = await this.orderModel.findOne({ - where: { externalOrderId, siteId: siteId }, - }); - // 如果订单已存在 - if (existingOrder) { - // 检查是否可以更新 ERP 状态 - if (this.canUpdateErpStatus(existingOrder.orderStatus)) { - entity.orderStatus = this.mapOrderStatus(entity.status); - } else { - // 如果不能更新 ERP 状态,则保留原有的 orderStatus - entity.orderStatus = existingOrder.orderStatus; - } - // 更新订单数据(包括 shipping、billing 等字段) - await this.orderModel.update(existingOrder.id, entity); - entity.id = existingOrder.id; - return entity; - } - // 如果订单不存在,则映射订单状态 - 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 }, - // }); - // if (!customer) { - // // 这里用 customer create - // await this.customerModel.save({ - // email: order.customer_email, - // site_id: siteId, - // origin_id: String(order.customer_id), - // billing: order.billing, - // shipping: order.shipping, - // phone: order?.billing?.phone || order?.shipping?.phone, - // }); - // } - // 保存新订单 - return await this.orderModel.save(entity); - } - - /** - * 检查是否可以更新 ERP 状态 - * 某些状态不允许被覆盖,如: AFTER_SALE_PROCESSING, PENDING_RESHIPMENT, PENDING_REFUND - * - * @param currentErpStatus 当前 ERP 状态 - * @returns 是否可以更新 - */ - canUpdateErpStatus(currentErpStatus: string): boolean { - const nonOverridableStatuses = [ - 'AFTER_SALE_PROCESSING', - 'PENDING_RESHIPMENT', - 'PENDING_REFUND', - ]; - // 如果当前 ERP 状态不可覆盖,则禁止更新 - return !nonOverridableStatuses.includes(currentErpStatus); - } - - /** - * 映射订单状态 - * 将 WooCommerce 订单状态转换为 ERP 订单状态 - * - * @param status WooCommerce 订单状态 - * @returns ERP 订单状态 - */ - mapOrderStatus(status: OrderStatus): ErpOrderStatus { - switch (status) { - case OrderStatus.PENDING: - return ErpOrderStatus.PENDING; - case OrderStatus.PROCESSING: - return ErpOrderStatus.PROCESSING; - case OrderStatus.COMPLETED: - return ErpOrderStatus.COMPLETED; - case OrderStatus.CANCEL: - return ErpOrderStatus.CANCEL; - case OrderStatus.REFUNDED: - return ErpOrderStatus.REFUNDED; - case OrderStatus.FAILED: - return ErpOrderStatus.FAILED; - case OrderStatus.RETURN_REQUESTED: - return ErpOrderStatus.RETURN_REQUESTED; - case OrderStatus.RETURN_APPROVED: - return ErpOrderStatus.RETURN_APPROVED; - case OrderStatus.RETURN_CANCELLED: - return ErpOrderStatus.RETURN_CANCELLED; - default: - return ErpOrderStatus.PENDING; - } - } - - /** - * 保存订单项 - * 流程说明: - * 1. 查询数据库中已存在的订单项 - * 2. 找出需要删除的订单项(在数据库中存在但在新数据中不存在) - * 3. 删除这些订单项及其对应的销售项(OrderSale) - * 4. 遍历新的订单项,设置相关字段 - * 5. 保存每个订单项 - * 6. 为每个订单项创建对应的销售项(OrderSale) - * - * 涉及实体: OrderItem, OrderSale - * - * @param params 订单项参数 - */ - async saveOrderItems(params: { - siteId: number; - orderId: number; - externalOrderId: string; - orderItems: Record[]; - }) { - // 查询数据库中已存在的订单项 - const { siteId, orderId, externalOrderId, orderItems } = params; - const currentOrderItems = await this.orderItemModel.find({ - where: { siteId, externalOrderId: externalOrderId }, - }); - // 找出需要删除的订单项(在数据库中存在但在新数据中不存在) - const syncedOrderItemIds = new Set(orderItems.map(v => String(v.id))); - const orderItemToDelete = currentOrderItems.filter( - db => !syncedOrderItemIds.has(String(db.externalOrderItemId)) - ); - // 删除这些订单项及其对应的销售项(OrderSale) - if (orderItemToDelete.length > 0) { - const idsToDelete = orderItemToDelete.map(v => v.id); - await this.orderItemModel.delete(idsToDelete); - const itemIdsToDelete = orderItemToDelete.map(v => v.externalOrderItemId); - await this.orderSaleModel.delete({ - siteId, - externalOrderItemId: In(itemIdsToDelete), - }); - } - // 遍历新的订单项,设置相关字段并保存 - for (const orderItem of orderItems) { - orderItem.siteId = siteId; - orderItem.externalOrderId = externalOrderId; - orderItem.externalOrderItemId = String(orderItem.id); - orderItem.orderId = orderId; - orderItem.externalProductId = String(orderItem.product_id); - orderItem.externalVariationId = String(orderItem.variation_id); - delete orderItem.id; - const entity = plainToClass(OrderItem, orderItem); - // 保存订单项 - await this.saveOrderItem(entity); - // 为每个订单项创建对应的销售项(OrderSale) - await this.saveOrderSale(entity); - } - } - - /** - * 保存单个订单项 - * 流程说明: - * 1. 检查数据库中是否已存在该订单项 - * 2. 如果存在,则更新订单项 - * 3. 如果不存在,则创建新订单项 - * - * 涉及实体: OrderItem - * - * @param orderItem 订单项实体 - */ - async saveOrderItem(orderItem: OrderItem) { - const existingOrderItem = await this.orderItemModel.findOne({ - where: { - externalOrderId: orderItem.externalOrderId, - siteId: orderItem.siteId, - externalOrderItemId: orderItem.externalOrderItemId, - }, - }); - - if ( - existingOrderItem && - existingOrderItem.quantity === orderItem.quantity - ) { - return; - } - if (existingOrderItem) { - await this.orderItemModel.update(existingOrderItem.id, orderItem); - } else { - await this.orderItemModel.save(orderItem); - } - } - - /** - * 保存单个订单项(备用方法) - * 流程说明: - * 1. 检查数据库中是否已存在该订单项 - * 2. 如果数量相同,则跳过处理 - * 3. 如果存在但数量不同,则更新订单项 - * 4. 如果不存在,则创建新订单项 - * - * 注意: 该方法与 saveOrderItem 功能相同,可能是备用或待删除的方法 - * - * 涉及实体: OrderItem - * - * @param orderItem 订单项实体 - */ - async saveOrderItemsG(orderItem: OrderItem) { - const existingOrderItem = await this.orderItemModel.findOne({ - where: { - externalOrderId: orderItem.externalOrderId, - siteId: orderItem.siteId, - externalOrderItemId: orderItem.externalOrderItemId, - }, - }); - - if ( - existingOrderItem && - existingOrderItem.quantity === orderItem.quantity - ) { - return; - } - if (existingOrderItem) { - await this.orderItemModel.update(existingOrderItem.id, orderItem); - } else { - await this.orderItemModel.save(orderItem); - } - } - - /** - * 保存订单销售项 - * 流程说明: - * 1. 查询数据库中已存在的销售项 - * 2. 删除已存在的销售项 - * 3. 如果订单项没有 SKU,则跳过处理 - * 4. 查询产品信息(包含组件信息) - * 5. 如果产品有组件,则为每个组件创建销售项 - * 6. 如果产品没有组件,则直接创建销售项 - * 7. 保存所有销售项 - * - * 涉及实体: OrderSale, Product - * - * @param orderItem 订单项实体 - */ - async saveOrderSale(orderItem: OrderItem) { - const currentOrderSale = await this.orderSaleModel.find({ - where: { - siteId: orderItem.siteId, - externalOrderItemId: orderItem.externalOrderItemId, - }, - }); - if (currentOrderSale.length > 0) { - 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'], - }); - - if (!product) return; - - const orderSales: OrderSale[] = []; - - if (product.components && product.components.length > 0) { - for (const comp of product.components) { - const baseProduct = await this.productModel.findOne({ - where: { sku: comp.sku }, - }); - if (baseProduct) { - const orderSaleItem: OrderSale = plainToClass(OrderSale, { - orderId: orderItem.orderId, - siteId: orderItem.siteId, - externalOrderItemId: orderItem.externalOrderItemId, - productId: baseProduct.id, - name: baseProduct.name, - quantity: comp.quantity * orderItem.quantity, - sku: comp.sku, - isPackage: orderItem.name.toLowerCase().includes('package'), - }); - orderSales.push(orderSaleItem); - } - } - } else { - const orderSaleItem: OrderSale = plainToClass(OrderSale, { - orderId: orderItem.orderId, - siteId: orderItem.siteId, - externalOrderItemId: orderItem.externalOrderItemId, - productId: product.id, - name: product.name, - quantity: orderItem.quantity, - sku: product.sku, - isPackage: orderItem.name.toLowerCase().includes('package'), - }); - orderSales.push(orderSaleItem); - } - - if (orderSales.length > 0) { - await this.orderSaleModel.save(orderSales); - } - } - - /** - * 保存订单退款信息 - * 流程说明: - * 1. 遍历退款列表,为每个退款项获取详细信息 - * 2. 设置退款相关字段(orderId、siteId、externalOrderId、externalRefundId) - * 3. 检查数据库中是否已存在该退款 - * 4. 如果存在,则更新退款信息 - * 5. 如果不存在,则创建新退款 - * 6. 保存退款项(OrderRefundItem) - * - * 涉及实体: OrderRefund, OrderRefundItem - * - * @param params 退款参数 - */ - async saveOrderRefunds({ - siteId, - orderId, - externalOrderId, - refunds, - }: { - siteId: number; - orderId: number; - externalOrderId: string; - refunds: Record[]; - }) { - for (const item of refunds) { - const refund = await this.wpService.getOrderRefund( - String(siteId), - externalOrderId, - item.id - ); - const refundItems = refund.line_items; - refund.orderId = orderId; - refund.siteId = siteId; - refund.externalOrderId = externalOrderId; - refund.externalRefundId = item.id; - delete refund.id; - const entity = plainToClass(OrderRefund, refund); - const existingOrderRefund = await this.orderRefundModel.findOne({ - where: { - siteId, - externalOrderId: externalOrderId, - externalRefundId: item.id, - }, - }); - let refundId; - if (existingOrderRefund) { - await this.orderRefundModel.update(existingOrderRefund.id, entity); - refundId = existingOrderRefund.id; - } else { - refundId = (await this.orderRefundModel.save(entity)).id; - } - this.saveOrderRefundItems({ - refundItems, - siteId, - refundId, - externalRefundId: item.id, - }); - } - } - - /** - * 保存订单退款项 - * 流程说明: - * 1. 遍历退款项列表 - * 2. 设置退款项相关字段(externalRefundItemId、externalProductId、externalVariationId) - * 3. 创建退款项实体 - * 4. 检查数据库中是否已存在该退款项 - * 5. 如果存在,则更新退款项 - * 6. 如果不存在,则创建新退款项 - * - * 涉及实体: OrderRefundItem - * - * @param params 退款项参数 - */ - async saveOrderRefundItems({ - refundItems, - siteId, - refundId, - externalRefundId, - }) { - for (const item of refundItems) { - item.externalRefundItemId = item.id; - item.externalProductId = item.product_id; - item.externalVariationId = item.variation_id; - delete item.id; - const refundItem = plainToClass(OrderRefundItem, { - refundId, - siteId, - externalRefundId, - ...item, - }); - const existingOrderRefundItem = await this.orderRefundItemModel.findOne({ - where: { - siteId, - externalRefundId, - externalRefundItemId: refundItem.externalRefundItemId, - }, - }); - if (existingOrderRefundItem) { - await this.orderRefundItemModel.update( - existingOrderRefundItem.id, - refundItem - ); - } else { - await this.orderRefundItemModel.save(refundItem); - } - } - } - - /** - * 保存订单费用信息 - * 流程说明: - * 1. 查询数据库中已存在的费用项 - * 2. 找出需要删除的费用项(在数据库中存在但在新数据中不存在) - * 3. 删除这些费用项 - * 4. 遍历新的费用项,设置相关字段 - * 5. 检查数据库中是否已存在该费用项 - * 6. 如果存在,则更新费用项 - * 7. 如果不存在,则创建新费用项 - * - * 涉及实体: OrderFee - * - * @param params 费用参数 - */ - async saveOrderFees({ siteId, orderId, externalOrderId, fee_lines }) { - const currentOrderFees = await this.orderFeeModel.find({ - where: { siteId, externalOrderId }, - }); - const syncedFeeIds = new Set(fee_lines.map(v => String(v.id))); - const toDeleteIds = currentOrderFees - .filter(db => !syncedFeeIds.has(db.externalOrderFeeId)) - .map(db => db.id); - if (toDeleteIds.length > 0) { - await this.orderFeeModel.delete(toDeleteIds); - } - for (const fee of fee_lines) { - fee.externalOrderFeeId = String(fee.id); - delete fee.id; - const entity = plainToClass(OrderFee, { - siteId, - orderId, - externalOrderId, - ...fee, - }); - const db = await this.orderFeeModel.findOne({ - where: { - siteId, - externalOrderId, - externalOrderFeeId: entity.externalOrderFeeId, - }, - }); - if (db) { - await this.orderFeeModel.update(db.id, entity); - } else { - await this.orderFeeModel.save(entity); - } - } - } - - /** - * 保存订单优惠券信息 - * 流程说明: - * 1. 遍历优惠券列表 - * 2. 设置优惠券相关字段(externalOrderCouponId、siteId、orderId、externalOrderId) - * 3. 创建优惠券实体 - * 4. 检查数据库中是否已存在该优惠券 - * 5. 如果存在,则更新优惠券 - * 6. 如果不存在,则创建新优惠券 - * - * 涉及实体: OrderCoupon - * - * @param params 优惠券参数 - */ - async saveOrderCoupons({ siteId, externalOrderId, orderId, coupon_lines }) { - for (const item of coupon_lines) { - item.externalOrderCouponId = item.id; - item.siteId = siteId; - item.orderId = orderId; - item.externalOrderId = externalOrderId; - delete item.id; - const coupon = plainToClass(OrderCoupon, item); - const existingOrderCoupon = await this.orderCouponModel.findOne({ - where: { - siteId, - externalOrderId, - externalOrderCouponId: coupon.externalOrderCouponId, - }, - }); - if (existingOrderCoupon) { - await this.orderCouponModel.update(existingOrderCoupon.id, coupon); - } else { - await this.orderCouponModel.save(coupon); - } - } - } - - /** - * 保存订单配送信息 - * 流程说明: - * 1. 遍历配送列表 - * 2. 设置配送相关字段(externalOrderShippingId、siteId、orderId、externalOrderId) - * 3. 创建配送实体 - * 4. 检查数据库中是否已存在该配送 - * 5. 如果存在,则更新配送 - * 6. 如果不存在,则创建新配送 - * - * 涉及实体: OrderShipping - * - * @param params 配送参数 - */ - async saveOrderShipping({ - siteId, - orderId, - externalOrderId, - shipping_lines, - }) { - for (const item of shipping_lines) { - item.externalOrderShippingId = item.id; - item.siteId = siteId; - item.orderId = orderId; - item.externalOrderId = externalOrderId; - delete item.id; - const shipping = plainToClass(OrderShipping, item); - const existingOrderShipping = await this.orderShippingModel.findOne({ - where: { - siteId, - externalOrderId, - externalOrderShippingId: shipping.externalOrderShippingId, - }, - }); - if (existingOrderShipping) { - await this.orderShippingModel.update( - existingOrderShipping.id, - shipping - ); - } else { - await this.orderShippingModel.save(shipping); - } - } - } - - /** - * 保存订单履约信息 - * 流程说明: - * 1. 检查履约列表是否存在,如果不存在则跳过 - * 2. 遍历履约列表 - * 3. 设置履约相关字段(externalFulfillmentId、siteId、orderId、externalOrderId) - * 4. 转换时间戳为日期格式 - * 5. 创建履约实体 - * 6. 检查数据库中是否已存在该履约 - * 7. 如果存在,则更新履约 - * 8. 如果不存在,则创建新履约 - * - * 涉及实体: OrderFulfillment - * - * @param params 履约参数 - */ - async saveOrderFulfillments({ - siteId, - orderId, - externalOrderId, - fulfillments, - }) { - // 如果履约列表不存在,则跳过处理 - if (!fulfillments || !Array.isArray(fulfillments) || fulfillments.length === 0) { - return; - } - - // 遍历履约列表 - for (const item of fulfillments) { - // 设置履约相关字段 - item.external_fulfillment_id = String(item.id); - item.site_id = siteId; - item.order_id = orderId; - item.external_order_id = String(externalOrderId); - - // 删除原始 ID - delete item.id; - - // 转换时间戳为日期格式 - if (item.date_created && typeof item.date_created === 'number') { - item.date_created = new Date(item.date_created * 1000); - } else if (item.date_created && typeof item.date_created === 'string') { - item.date_created = new Date(item.date_created); - } - - // 创建履约实体 - const fulfillment = plainToClass(OrderFulfillment, item); - - // 检查数据库中是否已存在该履约 - const existingOrderFulfillment = await this.orderFulfillmentModel.findOne({ - where: { - site_id: siteId, - external_order_id: externalOrderId, - external_fulfillment_id: fulfillment.external_fulfillment_id, - }, - }); - - // 如果存在,则更新履约 - if (existingOrderFulfillment) { - await this.orderFulfillmentModel.update( - existingOrderFulfillment.id, - fulfillment - ); - } else { - // 如果不存在,则创建新履约 - await this.orderFulfillmentModel.save(fulfillment); - } - } - } - - /** - * 获取订单列表 - * 流程说明: - * 1. 构建基础SQL查询,包含订单基本信息、客户统计、订阅信息、物流信息 - * 2. 根据参数动态添加过滤条件(订单ID、站点ID、日期范围、状态、关键字等) - * 3. 检查用户权限,如果用户有"order-10-days"权限且未指定日期范围,则限制查询最近10天的订单 - * 4. 执行总数查询 - * 5. 添加分页和排序,执行主查询 - * 6. 返回订单列表和分页信息 - * - * 涉及实体: Order, Subscription, Shipment, Customer - * - * @param params 查询参数 - * @param userId 用户ID(用于权限检查) - * @returns 订单列表和分页信息 - */ - async getOrders({ - externalOrderId, - siteId, - startDate, - endDate, - status, - keyword, - current, - pageSize, - customer_email, - payment_method, - billing_phone, - isSubscriptionOnly = false, - }, userId = undefined) { - const parameters: any[] = []; - - // 基础查询 - let sqlQuery = ` - SELECT - o.id as id, - o.externalOrderId as externalOrderId, - o.siteId as siteId, - o.date_paid as date_paid, - o.total as total, - o.date_created as date_created, - o.customer_email as customer_email, - o.exchange_frequency as exchange_frequency, - o.transaction_id as transaction_id, - o.orderStatus as orderStatus, - o.customer_ip_address as customer_ip_address, - o.device_type as device_type, - o.source_type as source_type, - o.utm_source as utm_source, - o.customer_note as customer_note, - o.shipping as shipping, - o.billing as billing, - o.payment_method as payment_method, - cs.order_count as order_count, - cs.total_spent as total_spent, - CASE WHEN EXISTS ( - SELECT 1 FROM subscription s - WHERE s.siteId = o.siteId AND s.parent_id = o.externalOrderId - ) THEN 1 ELSE 0 END AS isSubscription, - ( - SELECT COALESCE( - JSON_ARRAYAGG( - JSON_OBJECT( - 'id', s.id, - 'externalSubscriptionId', s.externalSubscriptionId, - 'status', s.status, - 'currency', s.currency, - 'total', s.total, - 'billing_period', s.billing_period, - 'billing_interval', s.billing_interval, - 'customer_id', s.customer_id, - 'customer_email', s.customer_email, - 'parent_id', s.parent_id, - 'start_date', s.start_date, - 'trial_end', s.trial_end, - 'next_payment_date', s.next_payment_date, - 'end_date', s.end_date, - 'line_items', s.line_items, - 'meta_data', s.meta_data - ) - ), - JSON_ARRAY() - ) - FROM subscription s - WHERE s.siteId = o.siteId AND s.parent_id = o.externalOrderId - ) AS related, - COALESCE( - JSON_ARRAYAGG( - CASE WHEN s.id IS NOT NULL THEN JSON_OBJECT( - 'state', s.state, - 'tracking_provider', s.tracking_provider, - 'primary_tracking_number', s.primary_tracking_number - ) END - ), - JSON_ARRAY() - ) as shipmentList, - COALESCE( - JSON_ARRAYAGG( - CASE WHEN fulfillment.id IS NOT NULL THEN JSON_OBJECT( - 'id', fulfillment.id, - 'externalFulfillmentId', fulfillment.external_fulfillment_id, - 'orderId', fulfillment.order_id, - 'siteId', fulfillment.site_id, - 'status', fulfillment.status, - 'createdAt', fulfillment.created_at, - 'updatedAt', fulfillment.updated_at, - 'tracking_number', fulfillment.tracking_number, - 'shipping_provider', fulfillment.shipping_provider, - 'shipping_method', fulfillment.shipping_method, - 'items', fulfillment.items - ) END - ), - JSON_ARRAY() - ) as fulfillments - FROM \`order\` o - LEFT JOIN ( - SELECT - o.customer_email AS customer_email, - COUNT(o.id) AS order_count, - SUM(o.total) AS total_spent - FROM \`order\` o - WHERE o.status IN ('processing', 'completed') - GROUP BY o.customer_email - ) cs ON cs.customer_email = o.customer_email - LEFT JOIN order_shipment os ON os.order_id = o.id - LEFT JOIN shipment s ON s.id = os.shipment_id - LEFT JOIN order_fulfillment fulfillment ON fulfillment.order_id = o.id - WHERE 1=1 - `; - - // 计算总数 - let totalQuery = ` - SELECT COUNT(*) as total - FROM \`order\` o - LEFT JOIN ( - SELECT o.customer_email AS customer_email - FROM \`order\` o - WHERE o.status IN ('processing', 'completed') - GROUP BY o.customer_email - ) cs ON cs.customer_email = o.customer_email - WHERE 1=1 - `; - - // 动态添加过滤条件 - if (externalOrderId) { - sqlQuery += ` AND o.externalOrderId = ?`; - totalQuery += ` AND o.externalOrderId = ?`; - parameters.push(externalOrderId); - } - if (siteId) { - sqlQuery += ` AND o.siteId = ?`; - totalQuery += ` AND o.siteId = ?`; - parameters.push(siteId); - } - if (startDate) { - sqlQuery += ` AND o.date_created >= ?`; - totalQuery += ` AND o.date_created >= ?`; - parameters.push(startDate); - } - if (endDate) { - sqlQuery += ` AND o.date_created <= ?`; - totalQuery += ` AND o.date_created <= ?`; - parameters.push(endDate); - } - // 支付方式筛选(使用参数化,避免SQL注入) - if (payment_method) { - sqlQuery += ` AND o.payment_method LIKE ?`; - totalQuery += ` AND o.payment_method LIKE ?`; - parameters.push(`%${payment_method}%`); - } - const user = await this.userModel.findOneBy({ id: userId }); - if (user?.permissions?.includes('order-10-days') && !startDate && !endDate) { - sqlQuery += ` AND o.date_created >= ?`; - totalQuery += ` AND o.date_created >= ?`; - const tenDaysAgo = new Date(); - tenDaysAgo.setDate(tenDaysAgo.getDate() - 10); - parameters.push(tenDaysAgo.toISOString()); - } - - // 处理 status 参数 - if (status) { - if (Array.isArray(status)) { - sqlQuery += ` AND o.orderStatus IN (${status - .map(() => '?') - .join(', ')})`; - totalQuery += ` AND o.orderStatus IN (${status - .map(() => '?') - .join(', ')})`; - parameters.push(...status); - } else { - sqlQuery += ` AND o.orderStatus = ?`; - totalQuery += ` AND o.orderStatus = ?`; - parameters.push(status); - } - } - - // 仅订阅订单过滤:父订阅订单 或 行项目包含订阅相关元数据(兼容 JSON 与字符串存储) - if (isSubscriptionOnly) { - const subCond = ` - AND ( - EXISTS ( - SELECT 1 FROM subscription s - WHERE s.siteId = o.siteId AND s.parent_id = o.externalOrderId - ) - - ) - `; - sqlQuery += subCond; - totalQuery += subCond; - } - - if (customer_email) { - sqlQuery += ` AND o.customer_email LIKE ?`; - totalQuery += ` AND o.customer_email LIKE ?`; - parameters.push(`%${customer_email}%`); - } - - if (billing_phone) { - sqlQuery += ` AND o.billing_phone LIKE ?`; - totalQuery += ` AND o.billing_phone LIKE ?`; - parameters.push(`%${billing_phone}%`); - } - - // 关键字搜索 - if (keyword) { - sqlQuery += ` - AND EXISTS ( - SELECT 1 FROM order_item oi - WHERE oi.orderId = o.id - AND oi.name LIKE ? - ) - `; - totalQuery += ` - AND EXISTS ( - SELECT 1 FROM order_item oi - WHERE oi.orderId = o.id - AND oi.name LIKE ? - ) - `; - parameters.push(`%${keyword}%`); - } - - // 执行获取总数的查询 - const totalResult = await this.orderModel.query(totalQuery, parameters); - const total = totalResult[0]?.total || 0; - - // 添加分页到主查询 - sqlQuery += ` - GROUP BY o.id - ORDER BY o.date_created DESC - LIMIT ? OFFSET ? - `; - parameters.push(pageSize, (current - 1) * pageSize); - - // 执行查询 - const orders = await this.orderModel.query(sqlQuery, parameters); - return { items: orders, total, current, pageSize }; - } - - /** - * 获取订单状态统计 - * 流程说明: - * 1. 构建查询,按订单状态分组统计订单数量 - * 2. 根据参数动态添加过滤条件(订单ID、站点ID、日期范围、关键字等) - * 3. 支持关键字搜索,检查订单项名称是否包含关键字 - * 4. 支持订阅订单过滤 - * 5. 返回各状态的订单数量统计 - * - * 涉及实体: Order, OrderItem, Subscription - * - * @param params 查询参数 - * @returns 订单状态统计列表 - */ - async getOrderStatus({ - externalOrderId, - siteId, - startDate, - endDate, - keyword, - customer_email, - billing_phone, - isSubscriptionOnly = false, - }: any) { - const query = this.orderModel - .createQueryBuilder('order') - .select('order.orderStatus', 'status') - .addSelect('COUNT(*)', 'count') - .groupBy('order.orderStatus'); - - if (externalOrderId) { - query.andWhere('order.externalOrderId = :externalOrderId', { - externalOrderId, - }); - } - if (siteId) { - query.andWhere('order.siteId = :siteId', { siteId }); - } - if (startDate) { - query.andWhere('order.date_created >= :startDate', { startDate }); - } - if (endDate) { - query.andWhere('order.date_created <= :endDate', { endDate }); - } - if (customer_email) - query.andWhere('order.customer_email LIKE :customer_email', { - customer_email: `%${customer_email}%`, - }); - - // 🔥 关键字搜索:检查 order_item.name 是否包含 keyword - if (keyword) { - query.andWhere( - `EXISTS ( - SELECT 1 FROM order_item oi - WHERE oi.orderId = order.id - AND oi.name LIKE :keyword - )`, - { keyword: `%${keyword}%` } - ); - } - - if (isSubscriptionOnly) { - query.andWhere(`( - EXISTS ( - SELECT 1 FROM subscription s - WHERE s.siteId = order.siteId AND s.parent_id = order.externalOrderId - ) - )`); - } - - return await query.getRawMany(); - } - - /** - * 获取订单销售统计 - * 流程说明: - * 1. 查询总条数(按产品ID去重) - * 2. 分页查询产品基础信息(产品ID、名称、总数量、订单数) - * 3. 批量统计当前页产品的历史复购情况(第1次、第2次、第3次、>3次订单的数量和盒数) - * 4. 统计总量(时间段内的总数量、YOONE各规格数量、ZEX数量) - * 5. 支持按产品名称关键字过滤 - * 6. 支持排除套餐订单(exceptPackage) - * - * 涉及实体: OrderSale, Order - * - * @param params 查询参数 - * @returns 销售统计和分页信息 - */ - 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'); - startDate = (startDate as any) || defaultStart as any; - endDate = (endDate as any) || defaultEnd as any; - const offset = (current - 1) * pageSize; - - // ------------------------- - // 1. 查询总条数 - // ------------------------- - const countParams: any[] = [startDate, endDate]; - let countSql = ` - SELECT COUNT(DISTINCT os.productId) AS totalCount - FROM order_sale os - INNER JOIN \`order\` o ON o.id = os.orderId - WHERE o.date_paid BETWEEN ? AND ? - AND o.status IN ('completed','processing') - `; - if (siteId) { - countSql += ' AND os.siteId = ?'; - countParams.push(siteId); - } - if (nameKeywords.length > 0) { - countSql += ' AND (' + nameKeywords.map(() => 'os.name LIKE ?').join(' AND ') + ')'; - countParams.push(...nameKeywords.map(w => `%${w}%`)); - } - const [countResult] = await this.orderSaleModel.query(countSql, countParams); - const totalCount = Number(countResult?.totalCount || 0); - - // ------------------------- - // 2. 分页查询 product 基础信息 - // ------------------------- - const itemParams: any[] = [startDate, endDate]; - let nameCondition = ''; - if (nameKeywords.length > 0) { - nameCondition = ' AND (' + nameKeywords.map(() => 'os.name LIKE ?').join(' AND ') + ')'; - itemParams.push(...nameKeywords.map(w => `%${w}%`)); - } - - let itemSql = ` - SELECT os.productId, os.name, 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 ? - AND o.status IN ('completed','processing') - `; - if (siteId) { - itemSql += ' AND os.siteId = ?'; - itemParams.push(siteId); - } - if (exceptPackage) { - itemSql += ` - AND os.orderId IN ( - SELECT orderId - FROM order_sale - GROUP BY orderId - HAVING COUNT(*) = 1 - ) - `; - } - itemSql += nameCondition; - itemSql += ` - GROUP BY os.productId, os.name - ORDER BY totalQuantity DESC - LIMIT ? OFFSET ? - `; - itemParams.push(pageSize, offset); - const items = await this.orderSaleModel.query(itemSql, itemParams); - - // ------------------------- - // 3. 批量统计当前页 product 历史复购 - // ------------------------- - if (items.length > 0) { - const productIds = items.map(i => i.productId); - const pcParams: any[] = [...productIds, startDate, endDate]; - if (siteId) pcParams.push(siteId); - - let pcSql = ` - SELECT - os.productId, - SUM(CASE WHEN t.purchaseIndex = 1 THEN os.quantity ELSE 0 END) AS firstOrderYOONEBoxCount, - COUNT(DISTINCT CASE WHEN t.purchaseIndex = 1 THEN os.orderId END) AS firstOrderCount, - SUM(CASE WHEN t.purchaseIndex = 2 THEN os.quantity ELSE 0 END) AS secondOrderYOONEBoxCount, - COUNT(DISTINCT CASE WHEN t.purchaseIndex = 2 THEN os.orderId END) AS secondOrderCount, - SUM(CASE WHEN t.purchaseIndex = 3 THEN os.quantity ELSE 0 END) AS thirdOrderYOONEBoxCount, - COUNT(DISTINCT CASE WHEN t.purchaseIndex = 3 THEN os.orderId END) AS thirdOrderCount, - SUM(CASE WHEN t.purchaseIndex > 3 THEN os.quantity ELSE 0 END) AS moreThirdOrderYOONEBoxCount, - COUNT(DISTINCT CASE WHEN t.purchaseIndex > 3 THEN os.orderId END) AS moreThirdOrderCount - FROM order_sale os - INNER JOIN ( - SELECT o2.id AS orderId, - @idx := IF(@prev_email = o2.customer_email, @idx + 1, 1) AS purchaseIndex, - @prev_email := o2.customer_email - FROM \`order\` o2 - CROSS JOIN (SELECT @idx := 0, @prev_email := '') vars - WHERE o2.status IN ('completed','processing') - ORDER BY o2.customer_email, o2.date_paid - ) t ON t.orderId = os.orderId - WHERE os.productId IN (${productIds.map(() => '?').join(',')}) - AND os.orderId IN ( - SELECT id FROM \`order\` - WHERE date_paid BETWEEN ? AND ? - ${siteId ? 'AND siteId = ?' : ''} - ) - `; - if (exceptPackage) { - pcSql += ` - AND os.orderId IN ( - SELECT orderId - FROM order_sale - GROUP BY orderId - HAVING COUNT(*) = 1 - ) - `; - } - pcSql += ` - GROUP BY os.productId - `; - - console.log('------3.5-----', pcSql, pcParams, exceptPackage); - const pcResults = await this.orderSaleModel.query(pcSql, pcParams); - - const pcMap = new Map(); - pcResults.forEach(r => pcMap.set(r.productId, r)); - items.forEach(i => { - const r = pcMap.get(i.productId) || {}; - i.firstOrderYOONEBoxCount = Number(r.firstOrderYOONEBoxCount || 0); - i.firstOrderCount = Number(r.firstOrderCount || 0); - i.secondOrderYOONEBoxCount = Number(r.secondOrderYOONEBoxCount || 0); - i.secondOrderCount = Number(r.secondOrderCount || 0); - i.thirdOrderYOONEBoxCount = Number(r.thirdOrderYOONEBoxCount || 0); - i.thirdOrderCount = Number(r.thirdOrderCount || 0); - i.moreThirdOrderYOONEBoxCount = Number(r.moreThirdOrderYOONEBoxCount || 0); - i.moreThirdOrderCount = Number(r.moreThirdOrderCount || 0); - }); - } - - // ------------------------- - // 4. 总量统计(时间段 + siteId) - // ------------------------- - const totalParams: any[] = [startDate, endDate]; - const yooneParams: any[] = [startDate, endDate]; - let totalSql = ` - SELECT - SUM(os.quantity) AS totalQuantity - FROM order_sale os - INNER JOIN \`order\` o ON o.id = os.orderId - WHERE o.date_paid BETWEEN ? AND ? - AND o.status IN ('completed','processing') - `; - 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 - FROM order_sale os - INNER JOIN \`order\` o ON o.id = os.orderId - WHERE o.date_paid BETWEEN ? AND ? - AND o.status IN ('completed','processing') - `; - - if (siteId) { - totalSql += ' AND os.siteId = ?'; - totalParams.push(siteId); - yooneSql += ' AND os.siteId = ?'; - yooneParams.push(siteId); - } - - if (nameKeywords.length > 0) { - totalSql += ' AND (' + nameKeywords.map(() => 'os.name LIKE ?').join(' AND ') + ')'; - totalParams.push(...nameKeywords.map(w => `%${w}%`)); - } - - const [totalResult] = await this.orderSaleModel.query(totalSql, totalParams); - const [yooneResult] = await this.orderSaleModel.query(yooneSql, yooneParams); - - return { - items, - total: totalCount, // ✅ 总条数 - totalQuantity: Number(totalResult.totalQuantity || 0), - yoone3Quantity: Number(yooneResult.yoone3Quantity || 0), - yoone6Quantity: Number(yooneResult.yoone6Quantity || 0), - yoone9Quantity: Number(yooneResult.yoone9Quantity || 0), - yoone12Quantity: Number(yooneResult.yoone12Quantity || 0), - yoone12QuantityNew: Number(yooneResult.yoone12QuantityNew || 0), - yoone15Quantity: Number(yooneResult.yoone15Quantity || 0), - yoone18Quantity: Number(yooneResult.yoone18Quantity || 0), - zexQuantity: Number(yooneResult.zexQuantity || 0), - current, - pageSize, - }; - } - - - /** - * 获取订单项统计 - * 流程说明: - * 1. 使用CTE计算每个客户对每个产品的购买次数 - * 2. 查询订单项统计信息(外部产品ID、变体ID、名称、总数量、订单数) - * 3. 统计不同购买次数的订单数(第1次、第2次、第3次、>3次) - * 4. 支持按产品名称关键字过滤 - * 5. 支持分页查询 - * 6. 返回订单项统计和分页信息 - * - * 涉及实体: OrderItem, Order - * - * @param params 查询参数 - * @returns 订单项统计和分页信息 - */ - async getOrderItems({ - siteId, - startDate, - endDate, - current, - pageSize, - name, - }: 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'); - startDate = (startDate as any) || defaultStart as any; - endDate = (endDate as any) || defaultEnd as any; - // 分页查询 - let sqlQuery = ` - WITH product_purchase_counts AS ( - SELECT o.customer_email,oi.siteId,oi.externalProductId,oi.externalVariationId, oi.name, COUNT(DISTINCT o.id,oi.siteId,oi.externalProductId,oi.externalVariationId) AS order_count - FROM \`order\` o - JOIN order_item oi ON o.id = oi.orderId - WHERE o.status IN ('completed', 'processing') - GROUP BY o.customer_email, oi.siteId,oi.externalProductId,oi.externalVariationId, oi.name - ) - SELECT - oi.externalProductId AS externalProductId, - oi.externalVariationId AS externalVariationId, - oi.name AS name, - SUM(oi.quantity) AS totalQuantity, - COUNT(distinct oi.orderId) AS totalOrders, - COUNT(DISTINCT CASE WHEN pc.order_count = 1 THEN o.id END) AS firstOrderCount, - COUNT(DISTINCT CASE WHEN pc.order_count = 2 THEN o.id END) AS secondOrderCount, - COUNT(DISTINCT CASE WHEN pc.order_count = 3 THEN o.id END) AS thirdOrderCount, - COUNT(DISTINCT CASE WHEN pc.order_count > 3 THEN o.id END) AS moreThirdOrderCount - FROM order_item oi - INNER JOIN \`order\` o ON o.id = oi.orderId - INNER JOIN product_purchase_counts pc ON pc.customer_email = o.customer_email AND pc.externalProductId = oi.externalProductId AND pc.externalVariationId = oi.externalVariationId - WHERE o.date_created BETWEEN ? AND ? - AND o.status IN ('processing', 'completed') - `; - const parameters: any[] = [startDate, endDate]; - if (siteId) { - sqlQuery += ' AND oi.siteId = ?'; - parameters.push(siteId); - } - if (nameKeywords.length > 0) { - sqlQuery += - ' AND ' + nameKeywords.map(() => `oi.name LIKE ?`).join(' AND '); - parameters.push(...nameKeywords.map(word => `%${word}%`)); - } - sqlQuery += ` - GROUP BY oi.siteId,oi.externalProductId,oi.externalVariationId, oi.name - ORDER BY totalQuantity DESC - `; - sqlQuery += ' LIMIT ? OFFSET ?'; - parameters.push(pageSize, (current - 1) * pageSize); - - // 执行查询并传递参数 - const items = await this.orderSaleModel.query(sqlQuery, parameters); - - let totalCountQuery = ` - SELECT COUNT(DISTINCT oi.siteId,oi.externalProductId,oi.externalVariationId) AS totalCount - FROM order_item oi - INNER JOIN \`order\` o ON o.id = oi.orderId - WHERE o.date_created BETWEEN ? AND ? - AND o.status IN ('processing', 'completed') - `; - const totalCountParameters: any[] = [startDate, endDate]; - if (siteId) { - totalCountQuery += ' AND oi.siteId = ?'; - totalCountParameters.push(siteId); - } - if (nameKeywords.length > 0) { - totalCountQuery += - ' AND ' + nameKeywords.map(() => `oi.name LIKE ?`).join(' AND '); - totalCountParameters.push(...nameKeywords.map(word => `%${word}%`)); - } - - const totalCountResult = await this.orderSaleModel.query( - totalCountQuery, - totalCountParameters - ); - - let totalQuantityQuery = ` - SELECT SUM(oi.quantity) AS totalQuantity - FROM order_item oi - INNER JOIN \`order\` o ON o.id = oi.orderId - WHERE o.date_created BETWEEN ? AND ? - AND o.status IN ('processing', 'completed') - `; - - const totalQuantityParameters: any[] = [startDate, endDate]; - if (siteId) { - totalQuantityQuery += ' AND oi.siteId = ?'; - totalQuantityParameters.push(siteId); - } - if (nameKeywords.length > 0) { - totalQuantityQuery += - ' AND ' + nameKeywords.map(() => `oi.name LIKE ?`).join(' AND '); - totalQuantityParameters.push(...nameKeywords.map(word => `%${word}%`)); - } - - const totalQuantityResult = await this.orderSaleModel.query( - totalQuantityQuery, - totalQuantityParameters - ); - - return { - items, - total: totalCountResult[0]?.totalCount, - totalQuantity: Number( - totalQuantityResult.reduce((sum, row) => sum + row.totalQuantity, 0) - ), - current, - pageSize, - }; - } - - /** - * 获取订单项列表 - * 流程说明: - * 1. 构建SQL查询,关联订单表,获取订单项和订单信息 - * 2. 检查订单项是否为订阅项(通过meta_data中的特定key判断) - * 3. 根据参数动态添加过滤条件(日期范围、站点ID、产品名称、外部产品ID、变体ID) - * 4. 执行总数查询 - * 5. 添加分页和排序,执行主查询 - * 6. 返回订单项列表和分页信息 - * - * 涉及实体: OrderItem, Order - * - * @param params 查询参数 - * @returns 订单项列表和分页信息 - */ - async getOrderItemList({ - siteId, - startDate, - endDate, - current, - pageSize, - name, - externalProductId, - externalVariationId, - }: any) { - const params: any[] = []; - let sql = ` - SELECT - oi.*, - o.id AS orderId, - o.externalOrderId AS orderExternalOrderId, - o.date_created AS orderDateCreated, - o.customer_email AS orderCustomerEmail, - o.orderStatus AS orderStatus, - o.siteId AS orderSiteId, - CASE WHEN - JSON_CONTAINS(JSON_EXTRACT(oi.meta_data, '$[*].key'), '"is_subscription"') - OR JSON_CONTAINS(JSON_EXTRACT(oi.meta_data, '$[*].key'), '"_wcs_bought_as_subscription"') - OR JSON_CONTAINS(JSON_EXTRACT(oi.meta_data, '$[*].key'), '"_wcsatt_scheme"') - OR JSON_CONTAINS(JSON_EXTRACT(oi.meta_data, '$[*].key'), '"_subscription"') - THEN 1 ELSE 0 END AS isSubscriptionItem - FROM order_item oi - INNER JOIN \`order\` o ON o.id = oi.orderId - WHERE 1=1 - `; - let countSql = ` - SELECT COUNT(*) AS total - FROM order_item oi - INNER JOIN \`order\` o ON o.id = oi.orderId - WHERE 1=1 - `; - const pushFilter = (cond: string, value: any) => { - sql += cond; countSql += cond; params.push(value); - }; - if (startDate) pushFilter(' AND o.date_created >= ?', startDate); - if (endDate) pushFilter(' AND o.date_created <= ?', endDate); - if (siteId) pushFilter(' AND oi.siteId = ?', siteId); - if (name) { - pushFilter(' AND oi.name LIKE ?', `%${name}%`); - } - if (externalProductId) pushFilter(' AND oi.externalProductId = ?', externalProductId); - if (externalVariationId) pushFilter(' AND oi.externalVariationId = ?', externalVariationId); - - sql += ' ORDER BY o.date_created DESC LIMIT ? OFFSET ?'; - const listParams = [...params, pageSize, (current - 1) * pageSize]; - - const items = await this.orderItemModel.query(sql, listParams); - const [countRow] = await this.orderItemModel.query(countSql, params); - const total = Number(countRow?.total || 0); - - return { items, total, current, pageSize }; - } - /** - * 获取订单详情 - * 流程说明: - * 1. 查询订单基本信息 - * 2. 查询站点信息 - * 3. 查询订单项 - * 4. 查询订单销售项 - * 5. 查询退款信息和退款项 - * 6. 查询订单备注(包含用户名) - * 7. 查询物流信息(包含物流项) - * 8. 查询关联的订阅和相关订单 - * 9. 返回完整的订单详情 - * - * 涉及实体: Order, Site, OrderItem, OrderSale, OrderRefund, OrderRefundItem, OrderNote, Shipment, ShipmentItem, Subscription - * - * @param id 订单ID - * @returns 订单详情 - */ - async getOrderDetail(id: number): Promise { - const order = await this.orderModel.findOne({ where: { id } }); - const site = await this.siteService.get(Number(order.siteId), true); - const items = await this.orderItemModel.find({ where: { orderId: id } }); - const sales = await this.orderSaleModel.find({ where: { orderId: id } }); - const refunds = await this.orderRefundModel.find({ - where: { orderId: id }, - }); - const refundItems = await this.orderRefundItemModel.find({ - where: { refundId: In(refunds.map(refund => refund.id)) }, - }); - const notes = await this.orderNoteModel - .createQueryBuilder('order_note') - .leftJoin(User, 'u', 'u.id=order_note.userId') - .select(['order_note.*', 'u.username as username']) - .where('order_note.orderId=:orderId', { orderId: id }) - .getRawMany(); - - const getShipmentSql = ` - SELECT - s.*, - CAST(CONCAT('[', GROUP_CONCAT(DISTINCT o.externalOrderId SEPARATOR ','), ']') AS JSON) AS orderIds - FROM - shipment s - JOIN - order_shipment os ON os.shipment_id = s.id - JOIN - \`order\` o ON os.order_id = o.id - WHERE - s.id IN ( - SELECT shipment_id FROM order_shipment WHERE order_id = ? - ) - GROUP BY - s.id; - `; - - const shipment = await this.shipmentModel.query(getShipmentSql, [id, id]); - if (shipment && shipment.length) { - for (const v of shipment) { - v.items = await this.shipmentItemModel.findBy({ shipment_id: v.id }); - } - } - - - - // 关联数据:订阅与相关订单(用于前端关联展示) - let relatedList: any[] = []; - try { - const related = await this.getRelatedByOrder(id); - const subs = Array.isArray(related?.subscriptions) ? related.subscriptions : []; - const ords = Array.isArray(related?.orders) ? related.orders : []; - const seen = new Set(); - const merge = [...subs, ...ords]; - for (const it of merge) { - const key = it?.externalSubscriptionId - ? `sub:${it.externalSubscriptionId}` - : it?.externalOrderId - ? `ord:${it.externalOrderId}` - : `id:${it?.id}`; - if (!seen.has(key)) { - seen.add(key); - relatedList.push(it); - } - } - } catch (error) { - // 关联查询失败不影响详情返回 - } - - return { - ...order, - name: site?.name, - // Site 实体无邮箱字段,这里返回空字符串保持兼容 - email: '', - items, - sales, - refundItems, - notes, - shipment, - related: relatedList, - }; - } - - /** - * 获取订单关联信息 - * 流程说明: - * 1. 查询订单基本信息 - * 2. 查询关联的订阅信息(通过parent_id关联) - * 3. 返回订单和订阅信息 - * - * 涉及实体: Order, Subscription - * - * @param orderId 订单ID - * @returns 订单和关联的订阅信息 - */ - async getRelatedByOrder(orderId: number) { - const order = await this.orderModel.findOne({ where: { id: orderId } }); - if (!order) throw new Error('订单不存在'); - const siteId = order.siteId; - const subSql = ` - SELECT * FROM subscription s - WHERE s.siteId = ? AND s.parent_id = ? - `; - const subscriptions = await this.orderModel.query(subSql, [siteId, order.externalOrderId]); - return { - order, - subscriptions, - orders: [], - }; - } - - /** - * 删除订单 - * 流程说明: - * 1. 查询订单是否存在 - * 2. 删除订单配送信息 - * 3. 删除订单销售项 - * 4. 删除退款信息和退款项 - * 5. 删除订单项 - * 6. 删除订单费用 - * 7. 删除订单优惠券 - * 8. 删除订单主数据 - * - * 涉及实体: Order, OrderShipping, OrderSale, OrderRefund, OrderRefundItem, OrderItem, OrderFee, OrderCoupon - * - * @param id 订单ID - */ - async delOrder(id: number) { - const order = await this.orderModel.findOne({ where: { id } }); - if (!order) throw new Error('订单不存在'); - await this.orderShippingModel.delete({ orderId: id }); - await this.orderSaleModel.delete({ orderId: id }); - const refunds = await this.orderRefundModel.find({ - where: { orderId: id }, - }); - if (refunds.length > 0) { - for (const refund of refunds) { - await this.orderRefundItemModel.delete({ refundId: refund.id }); - await this.orderRefundModel.delete({ id: refund.id }); - } - } - await this.orderItemModel.delete({ orderId: id }); - await this.orderFeeModel.delete({ orderId: id }); - await this.orderCouponModel.delete({ orderId: id }); - await this.orderModel.delete({ id }); - } - - /** - * 创建订单备注 - * 流程说明: - * 1. 接收用户ID和备注数据 - * 2. 创建订单备注实体,关联用户ID - * 3. 保存订单备注 - * - * 涉及实体: OrderNote - * - * @param userId 用户ID - * @param data 备注数据 - * @returns 保存后的订单备注 - */ - async createNote(userId: number, data: CreateOrderNoteDTO) { - return await this.orderNoteModel.save({ - ...data, - userId, - }); - } - - /** - * 根据订单号获取订单 - * 流程说明: - * 1. 根据订单号模糊查询订单(仅查询处理中和待补发的订单) - * 2. 批量获取订单涉及的站点名称 - * 3. 构建站点ID到站点名称的映射 - * 4. 返回订单列表,包含订单号、ID和站点名称 - * - * 涉及实体: Order, Site - * - * @param id 订单号 - * @returns 订单列表(包含订单号、ID和站点名称) - */ - async getOrderByNumber(id: string) { - const orders = await this.orderModel.find({ - where: { - externalOrderId: Like(id), - orderStatus: In([ - ErpOrderStatus.PROCESSING, - ErpOrderStatus.PENDING_RESHIPMENT, - ]), - }, - }); - // 批量获取订单涉及的站点名称,避免使用配置文件 - const siteIds = Array.from(new Set(orders.map(o => o.siteId).filter(Boolean))); - const { items: sites } = await this.siteService.list({ current: 1, pageSize: 1000, ids: siteIds.join(',') }, false); - const siteMap = new Map(sites.map((s: any) => [String(s.id), s.name])); - return orders.map(order => ({ - externalOrderId: order.externalOrderId, - id: order.id, - name: siteMap.get(String(order.siteId)) || '', - })); - } - - /** - * 取消订单 - * 流程说明: - * 1. 查询订单是否存在 - * 2. 查询订单所属站点 - * 3. 如果订单状态不是已取消,则调用WooCommerce API更新订单状态为已取消 - * 4. 更新本地订单状态为已取消 - * 5. 更新订单ERP状态为已取消 - * - * 涉及实体: Order, Site - * - * @param id 订单ID - */ - async cancelOrder(id: number) { - const order = await this.orderModel.findOne({ where: { id } }); - if (!order) throw new Error(`订单 ${id}不存在`); - const site = await this.siteService.get(Number(order.siteId), true); - if (order.status !== OrderStatus.CANCEL) { - await this.wpService.updateOrder(site, order.externalOrderId, { - status: OrderStatus.CANCEL, - }); - order.status = OrderStatus.CANCEL; - } - order.orderStatus = ErpOrderStatus.CANCEL; - await this.orderModel.save(order); - } - - /** - * 退款订单 - * 流程说明: - * 1. 查询订单是否存在 - * 2. 查询订单所属站点 - * 3. 如果订单状态不是已退款,则调用WooCommerce API更新订单状态为已退款 - * 4. 更新本地订单状态为已退款 - * 5. 更新订单ERP状态为已退款 - * - * 涉及实体: Order, Site - * - * @param id 订单ID - */ - async refundOrder(id: number) { - const order = await this.orderModel.findOne({ where: { id } }); - if (!order) throw new Error(`订单 ${id}不存在`); - const site = await this.siteService.get(Number(order.siteId), true); - if (order.status !== OrderStatus.REFUNDED) { - await this.wpService.updateOrder(site, order.externalOrderId, { - status: OrderStatus.REFUNDED, - }); - order.status = OrderStatus.REFUNDED; - } - order.orderStatus = ErpOrderStatus.REFUNDED; - await this.orderModel.save(order); - } - - /** - * 完成订单 - * 流程说明: - * 1. 查询订单是否存在 - * 2. 查询订单所属站点 - * 3. 如果订单状态不是已完成,则调用WooCommerce API更新订单状态为已完成 - * 4. 更新本地订单状态为已完成 - * 5. 更新订单ERP状态为已完成 - * - * 涉及实体: Order, Site - * - * @param id 订单ID - */ - async completedOrder(id: number) { - const order = await this.orderModel.findOne({ where: { id } }); - if (!order) throw new Error(`订单 ${id}不存在`); - const site = await this.siteService.get(order.siteId); - if (order.status !== OrderStatus.COMPLETED) { - await this.wpService.updateOrder(site, order.externalOrderId, { - status: OrderStatus.COMPLETED, - }); - order.status = OrderStatus.COMPLETED; - } - order.orderStatus = ErpOrderStatus.COMPLETED; - await this.orderModel.save(order); - } - - /** - * 更改订单状态 - * 流程说明: - * 1. 查询订单是否存在 - * 2. 更新订单ERP状态 - * 3. 保存订单 - * - * 涉及实体: Order - * - * @param id 订单ID - * @param status ERP订单状态 - */ - async changeStatus(id: number, status: ErpOrderStatus) { - const order = await this.orderModel.findOne({ where: { id } }); - if (!order) throw new Error(`订单 ${id}不存在`); - order.orderStatus = status; - await this.orderModel.save(order); - } - - - /** - * 创建订单 - * 流程说明: - * 1. 验证必需参数siteId是否存在 - * 2. 获取默认数据源 - * 3. 在事务中处理订单创建 - * 4. 保存订单基本信息(站点ID、外部订单号、状态、货币、日期、客户信息等) - * 5. 遍历销售项目列表 - * 6. 根据SKU查询产品信息 - * 7. 保存订单销售项(关联订单ID、站点ID、产品ID、名称、SKU、数量等) - * - * 涉及实体: Order, OrderSale, Product - * - * @param data 订单数据 - * @returns 创建的订单 - */ - async createOrder(data: Record) { - // 从数据中解构出需要用的属性 - const { siteId, sales, total, billing, customer_email, billing_phone } = data; - // 如果没有 siteId,则抛出错误 - if (!siteId) { - throw new Error('siteId is required'); - } - // 获取默认数据源 - const dataSource = this.dataSourceManager.getDataSource('default'); - const now = new Date(); - // 在事务中处理订单创建 - return dataSource.transaction(async manager => { - const orderRepo = manager.getRepository(Order); - const orderSaleRepo = manager.getRepository(OrderSale); - const productRepo = manager.getRepository(Product); - // 保存订单信息 - const order = await orderRepo.save({ - siteId, - externalOrderId: '-1', - status: OrderStatus.PROCESSING, - orderStatus: ErpOrderStatus.PROCESSING, - currency: 'CAD', - currency_symbol: '$', - date_created: now, - date_paid: now, - total, - customer_email, - billing_phone, - billing, - shipping: billing, - }); - // 遍历销售项目并保存 - for (const sale of sales) { - const product = await productRepo.findOne({ where: { sku: sale.sku } }); - const saleItem = { - orderId: order.id, - siteId: order.siteId, - externalOrderItemId: '-1', - productId: product.id, - name: product.name, - sku: sale.sku, - quantity: sale.quantity, - }; - await orderSaleRepo.save(saleItem); - } - }); - } - - /** - * 获取待处理订单项统计 - * 流程说明: - * 1. 构建SQL查询,关联订单表和订单销售表 - * 2. 按产品名称分组,统计每个产品的总数量和订单号列表 - * 3. 只查询状态为"处理中"的订单 - * 4. 执行总数查询 - * 5. 添加分页,执行主查询 - * 6. 返回待处理订单项统计和分页信息 - * - * 涉及实体: Order, OrderSale - * - * @param data 查询参数 - * @returns 待处理订单项统计和分页信息 - */ - async pengdingItems(data: Record) { - const { current = 1, pageSize = 10 } = data; - const sql = ` - SELECT - os.name, - SUM(os.quantity) AS quantity, - JSON_ARRAYAGG(os.orderId) AS numbers - FROM \`order\` o - INNER JOIN order_sale os ON os.orderId = o.id - WHERE o.status = 'processing' - GROUP BY os.name - LIMIT ${pageSize} OFFSET ${(current - 1) * pageSize} - `; - - const countSql = ` - SELECT COUNT(*) AS total FROM ( - SELECT 1 - FROM \`order\` o - INNER JOIN order_sale os ON os.orderId = o.id - WHERE o.status = 'processing' - GROUP BY os.name - ) AS temp - `; - const [items, countResult] = await Promise.all([ - this.orderModel.query(sql), - this.orderModel.query(countSql), - ]); - - const total = countResult[0]?.total || 0; - return { - items, - total, - current, - pageSize, - }; - } - - /** - * 更新订单销售项 - * 流程说明: - * 1. 获取默认数据源 - * 2. 在事务中处理订单销售项更新 - * 3. 查询订单信息 - * 4. 删除该订单的所有销售项 - * 5. 遍历新的销售项列表 - * 6. 根据SKU查询产品信息 - * 7. 保存新的订单销售项(关联订单ID、站点ID、产品ID、名称、SKU、数量等) - * 8. 处理事务错误 - * - * 涉及实体: Order, OrderSale, Product - * - * @param orderId 订单ID - * @param sales 销售项列表 - * @returns 更新成功返回true - */ - async updateOrderSales(orderId: number, sales: OrderSale[]) { - try { - const dataSource = this.dataSourceManager.getDataSource('default'); - let transactionError = undefined; - - await dataSource.transaction(async manager => { - const orderRepo = manager.getRepository(Order); - const orderSaleRepo = manager.getRepository(OrderSale); - const productRepo = manager.getRepository(Product); - - const order = await orderRepo.findOneBy({ id: orderId }); - let product: Product; - await orderSaleRepo.delete({ orderId }); - for (const sale of sales) { - product = await productRepo.findOneBy({ sku: sale.sku }); - await orderSaleRepo.save({ - orderId, - siteId: order.siteId, - productId: product.id, - name: product.name, - sku: sale.sku, - quantity: sale.quantity, - // externalOrderItemId: - }); - }; - - - }).catch(error => { - transactionError = error; - }); - - if (transactionError !== undefined) { - throw new Error(`更新物流信息错误:${transactionError.message}`); - } - return true; - } catch (error) { - throw new Error(`更新发货产品失败:${error.message}`); - } - } - - /** - * 更新换货订单 - * 流程说明: - * 1. 该方法用于换货确认功能 - * 2. 需要更新OrderSale和OrderItem数据 - * 3. 当前方法暂未实现 - * - * 涉及实体: Order, OrderSale, OrderItem, Product - * - * @param orderId 订单ID - * @param data 换货数据 - * @returns 更新成功返回true - */ - //换货确认按钮改成调用这个方法 - //换货功能更新OrderSale和Orderitem数据 - async updateExchangeOrder(orderId: number, data: any) { - throw new Error('暂未实现') - // try { - // const dataSource = this.dataSourceManager.getDataSource('default'); - // let transactionError = undefined; - - // await dataSource.transaction(async manager => { - // const orderRepo = manager.getRepository(Order); - // const orderSaleRepo = manager.getRepository(OrderSale); - // const orderItemRepo = manager.getRepository(OrderItem); - - - // const productRepo = manager.getRepository(ProductV2); - - // const order = await orderRepo.findOneBy({ id: orderId }); - // let product: ProductV2; - - // await orderSaleRepo.delete({ orderId }); - // await orderItemRepo.delete({ orderId }); - // for (const sale of data['sales']) { - // product = await productRepo.findOneBy({ sku: sale['sku'] }); - // await orderSaleRepo.save({ - // orderId, - // siteId: order.siteId, - // productId: product.id, - // name: product.name, - // sku: sale['sku'], - // quantity: sale['quantity'], - // }); - // }; - - // for (const item of data['items']) { - // product = await productRepo.findOneBy({ sku: item['sku'] }); - - // await orderItemRepo.save({ - // orderId, - // siteId: order.siteId, - // productId: product.id, - // name: product.name, - // externalOrderId: order.externalOrderId, - // externalProductId: product.externalProductId, - - // sku: item['sku'], - // quantity: item['quantity'], - // }); - - // }; - - // //将是否换货状态改为true - // await orderRepo.update( - // order.id - // , { - // is_exchange: true - // }); - - // //查询这个用户换过多少次货 - // const counts = await orderRepo.countBy({ - // is_editable: true, - // customer_email: order.customer_email, - // }); - - // //批量更新当前用户换货次数 - // await orderRepo.update({ - // customer_email: order.customer_email - // }, { - // exchange_frequency: counts - // }); - - // }).catch(error => { - // transactionError = error; - // }); - - // if (transactionError !== undefined) { - // throw new Error(`更新物流信息错误:${transactionError.message}`); - // } - // return true; - // } catch (error) { - // throw new Error(`更新发货产品失败:${error.message}`); - // } - } - - /** - * 导出订单为CSV格式 - * 流程说明: - * 1. 在事务中处理订单导出 - * 2. 根据订单ID列表查询订单信息(包含物流关联) - * 3. 查询所有订单项 - * 4. 按订单ID分组订单项 - * 5. 构建导出数据,包含以下字段: - * - 日期: 订单创建日期 - * - 订单号: 外部订单号 - * - 姓名地址: 收货人姓名和地址 - * - 邮箱: 客户邮箱 - * - 号码: 电话号码 - * - 订单内容: 产品名称和数量 - * - 盒数: 总盒数 - * - 换盒数: 换货盒数(当前默认为0) - * - 换货内容: 换货内容(当前默认为空) - * - 快递号: 物流追踪号 - * 6. 调用exportToCsv方法将数据转换为CSV格式 - * 7. 返回CSV字符串内容 - * - * 涉及实体: Order, OrderItem, Shipment - * - * @param ids 订单ID列表 - * @returns CSV格式字符串 - */ - // TODO - async exportOrder(ids: number[]) { - // 日期 订单号 姓名地址 邮箱 号码 订单内容 盒数 换盒数 换货内容 快递号 - interface ExportData { - '日期': string; - '订单号': string; - '姓名地址': string; - '邮箱': string; - '号码': string; - '订单内容': string; - '盒数': number; - '换盒数': number; - '换货内容': string; - '快递号': string; - } - - try { - - // 过滤掉NaN和非数字值,只保留有效的数字ID - const validIds = ids?.filter?.(id => Number.isFinite(id) && id > 0); - - const dataSource = this.dataSourceManager.getDataSource('default'); - - // 优化事务使用 - return await dataSource.transaction(async manager => { - // 准备查询条件 - const whereCondition: any = {}; - if (validIds.length > 0) { - whereCondition.id = In(validIds); - } - - // 获取订单、订单项和物流信息 - const orders = await manager.getRepository(Order).find({ - where: whereCondition, - relations: ['shipment'] - }); - - if (orders.length === 0) { - throw new Error('未找到匹配的订单'); - } - - // 获取所有订单ID - const orderIds = orders.map(order => order.id); - - // 获取所有订单项 - const orderItems = await manager.getRepository(OrderItem).find({ - where: { - orderId: In(orderIds) - } - }); - - // 按订单ID分组订单项 - const orderItemsByOrderId = orderItems.reduce((acc, item) => { - if (!acc[item.orderId]) { - acc[item.orderId] = []; - } - acc[item.orderId].push(item); - return acc; - }, {} as Record); - - // 构建导出数据 - const exportDataList: ExportData[] = orders.map(order => { - // 获取订单的订单项 - const items = orderItemsByOrderId[order.id] || []; - - // 计算总盒数 - const boxCount = items.reduce((total, item) => total + item.quantity, 0); - - // 构建订单内容 - const orderContent = items.map(item => `${item.name} (${item.sku || ''}) x ${item.quantity}`).join('; '); - - // 构建姓名地址 - const shipping = order.shipping; - const billing = order.billing; - const firstName = shipping?.first_name || billing?.first_name || ''; - const lastName = shipping?.last_name || billing?.last_name || ''; - const name = `${firstName} ${lastName}`.trim() || ''; - const address = shipping?.address_1 || billing?.address_1 || ''; - const address2 = shipping?.address_2 || billing?.address_2 || ''; - const city = shipping?.city || billing?.city || ''; - const state = shipping?.state || billing?.state || ''; - const postcode = shipping?.postcode || billing?.postcode || ''; - const country = shipping?.country || billing?.country || ''; - const nameAddress = `${name} ${address} ${address2} ${city} ${state} ${postcode} ${country}`; - - // 获取电话号码 - const phone = shipping?.phone || billing?.phone || ''; - - // 获取快递号 - const trackingNumber = order.shipment?.tracking_id || ''; - - // 暂时没有换货相关数据,默认为0和空字符串 - const exchangeBoxCount = 0; - const exchangeContent = ''; - - return { - '日期': order.date_created?.toISOString().split('T')[0] || '', - '订单号': order.externalOrderId || '', - '姓名地址': nameAddress, - '邮箱': order.customer_email || '', - '号码': phone, - '订单内容': orderContent, - '盒数': boxCount, - '换盒数': exchangeBoxCount, - '换货内容': exchangeContent, - '快递号': trackingNumber - }; - }); - - // 返回CSV字符串内容给前端 - const csvContent = await this.exportToCsv(exportDataList, { type: 'string' }); - return csvContent; - }); - } catch (error) { - throw new Error(`导出订单失败:${error.message}`); - } - } - - - /** - * 导出数据为CSV格式 - * @param {any[]} data 数据数组 - * @param {Object} options 配置选项 - * @param {string} [options.type='string'] 输出类型:'string' | 'buffer' - * @param {string} [options.fileName] 文件名(仅当需要写入文件时使用) - * @param {boolean} [options.writeFile=false] 是否写入文件 - * @returns {string|Buffer} 根据type返回字符串或Buffer - */ - async exportToCsv(data: any[], options: { type?: 'string' | 'buffer'; fileName?: string; writeFile?: boolean } = {}): Promise { - try { - // 检查数据是否为空 - if (!data || data.length === 0) { - throw new Error('导出数据不能为空'); - } - - const { type = 'string', fileName, writeFile = false } = options; - - // 生成表头 - const headers = Object.keys(data[0]); - let csvContent = headers.join(',') + '\n'; - - // 处理数据行 - data.forEach(item => { - const row = headers.map(key => { - const value = item[key as keyof any]; - // 处理特殊字符 - if (typeof value === 'string') { - // 转义双引号,将"替换为"" - const escapedValue = value.replace(/"/g, '""'); - // 如果包含逗号或换行符,需要用双引号包裹 - if (escapedValue.includes(',') || escapedValue.includes('\n')) { - return `"${escapedValue}"`; - } - return escapedValue; - } - // 处理日期类型 - if (value instanceof Date) { - return value.toISOString(); - } - // 处理undefined和null - if (value === undefined || value === null) { - return ''; - } - return String(value); - }).join(','); - csvContent += row + '\n'; - }); - - // 如果需要写入文件 - if (writeFile && fileName) { - // 获取当前用户目录 - const userHomeDir = os.homedir(); - - // 构建目标路径(下载目录) - const downloadsDir = path.join(userHomeDir, 'Downloads'); - - // 确保下载目录存在 - if (!fs.existsSync(downloadsDir)) { - fs.mkdirSync(downloadsDir, { recursive: true }); - } - - const filePath = path.join(downloadsDir, fileName); - - // 写入文件 - fs.writeFileSync(filePath, csvContent, 'utf8'); - - console.log(`数据已成功导出至 ${filePath}`); - return filePath; - } - - // 根据类型返回不同结果 - if (type === 'buffer') { - return Buffer.from(csvContent, 'utf8'); - } - - return csvContent; - } catch (error) { - console.error('导出CSV时出错:', error); - throw new Error(`导出CSV文件失败: ${error.message}`); - } - } - - - -} diff --git a/src/service/order.service.ts b/src/service/order.service.ts index 586dc42..586a5b4 100644 --- a/src/service/order.service.ts +++ b/src/service/order.service.ts @@ -141,7 +141,7 @@ export class OrderService { updated: 0, errors: [] }; - + console.log('开始进入循环同步订单', result.length, '个订单') // 遍历每个订单进行同步 for (const order of result) { try { @@ -165,6 +165,7 @@ export class OrderService { } else { syncResult.created++; } + // console.log('updated', syncResult.updated, 'created:', syncResult.created) } catch (error) { // 记录错误但不中断整个同步过程 syncResult.errors.push({ @@ -174,6 +175,8 @@ export class OrderService { syncResult.processed++; } } + console.log('同步完成', syncResult.updated, 'created:', syncResult.created) + this.logger.debug('syncOrders result', syncResult) return syncResult; } @@ -350,14 +353,14 @@ export class OrderService { await this.saveOrderItems({ siteId, orderId, - externalOrderId, + externalOrderId: String(externalOrderId), orderItems: line_items, }); // 保存退款信息 await this.saveOrderRefunds({ siteId, orderId, - externalOrderId, + externalOrderId , refunds, }); // 保存费用信息 @@ -1229,13 +1232,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注入) @@ -1323,7 +1326,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); @@ -2541,7 +2544,7 @@ export class OrderService { '姓名地址': nameAddress, '邮箱': order.customer_email || '', '号码': phone, - '订单内容': orderContent, + '订单内容': this.removeLastParenthesesContent(orderContent), '盒数': boxCount, '换盒数': exchangeBoxCount, '换货内容': exchangeContent, @@ -2641,4 +2644,80 @@ export class OrderService { throw new Error(`导出CSV文件失败: ${error.message}`); } } + + /** + * 删除每个分号前面一个左右括号和最后一个左右括号包含的内容(包括括号本身) + * @param str 输入字符串 + * @returns 删除后的字符串 + */ + removeLastParenthesesContent(str: string): string { + if (!str || typeof str !== 'string') { + return str; + } + + // 辅助函数:删除指定位置的括号对及其内容 + const removeParenthesesAt = (s: string, leftIndex: number): string => { + if (leftIndex === -1) return s; + + let rightIndex = -1; + let parenCount = 0; + + for (let i = leftIndex; i < s.length; i++) { + const char = s[i]; + if (char === '(') { + parenCount++; + } else if (char === ')') { + parenCount--; + if (parenCount === 0) { + rightIndex = i; + break; + } + } + } + + if (rightIndex !== -1) { + return s.substring(0, leftIndex) + s.substring(rightIndex + 1); + } + + return s; + }; + + // 1. 处理每个分号前面的括号对 + let result = str; + + // 找出所有分号的位置 + const semicolonIndices: number[] = []; + for (let i = 0; i < result.length; i++) { + if (result[i] === ';') { + semicolonIndices.push(i); + } + } + + // 从后向前处理每个分号,避免位置变化影响后续处理 + for (let i = semicolonIndices.length - 1; i >= 0; i--) { + const semicolonIndex = semicolonIndices[i]; + + // 从分号位置向前查找最近的左括号 + let lastLeftParenIndex = -1; + for (let j = semicolonIndex - 1; j >= 0; j--) { + if (result[j] === '(') { + lastLeftParenIndex = j; + break; + } + } + + // 如果找到左括号,删除该括号对及其内容 + if (lastLeftParenIndex !== -1) { + result = removeParenthesesAt(result, lastLeftParenIndex); + } + } + + // 2. 处理整个字符串的最后一个括号对 + let lastLeftParenIndex = result.lastIndexOf('('); + if (lastLeftParenIndex !== -1) { + result = removeParenthesesAt(result, lastLeftParenIndex); + } + + return result; + } } diff --git a/src/service/shopyy.service.ts b/src/service/shopyy.service.ts index 275e823..1e623f2 100644 --- a/src/service/shopyy.service.ts +++ b/src/service/shopyy.service.ts @@ -313,7 +313,6 @@ export class ShopyyService { 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); diff --git a/test-freightwaves.js b/test-freightwaves.js new file mode 100644 index 0000000..e2a537a --- /dev/null +++ b/test-freightwaves.js @@ -0,0 +1,26 @@ +// Test script for FreightwavesService createOrder method + +const { FreightwavesService } = require('./dist/service/test-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(); \ No newline at end of file