From 185a786b2eddcd784af2b585bb112d06f083ee0f Mon Sep 17 00:00:00 2001 From: tikkhun Date: Wed, 24 Dec 2025 14:50:56 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E7=A7=BB=E9=99=A4=E5=BA=9F?= =?UTF-8?q?=E5=BC=83=E7=9A=84WordPress=E4=BA=A7=E5=93=81=E7=9B=B8=E5=85=B3?= =?UTF-8?q?=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 清理不再使用的WordPress产品模块代码,包括实体、DTO、服务和控制器 统一使用新的产品模块接口 --- src/config/config.default.ts | 2 - src/controller/product.controller.ts | 15 - src/controller/wp_product.controller.ts | 232 ----- src/dto/customer.dto.ts | 8 + src/dto/reponse.dto.ts | 10 - src/dto/site-api.dto.ts | 3 - src/dto/woocommerce.dto.ts | 5 + src/dto/wp_product.dto.ts | 136 --- src/entity/product.entity.ts | 2 +- src/entity/product_site_sku.entity.ts | 2 +- src/service/customer.service.ts | 9 +- src/service/order.service.ts | 135 ++- src/service/product.service.ts | 9 - src/service/wp.service.ts | 9 +- src/service/wp_product.service.ts | 1214 ----------------------- tsconfig.tsbuildinfo | 2 +- 16 files changed, 89 insertions(+), 1704 deletions(-) delete mode 100644 src/controller/wp_product.controller.ts delete mode 100644 src/dto/wp_product.dto.ts delete mode 100644 src/service/wp_product.service.ts diff --git a/src/config/config.default.ts b/src/config/config.default.ts index e386aa6..de114e0 100644 --- a/src/config/config.default.ts +++ b/src/config/config.default.ts @@ -1,7 +1,6 @@ import { MidwayConfig } from '@midwayjs/core'; import { join } from 'path'; import { Product } from '../entity/product.entity'; -import { WpProduct } from '../entity/wp_product.entity'; import { Variation } from '../entity/variation.entity'; import { User } from '../entity/user.entity'; import { PurchaseOrder } from '../entity/purchase_order.entity'; @@ -53,7 +52,6 @@ export default { Product, ProductStockComponent, ProductSiteSku, - WpProduct, Variation, User, PurchaseOrder, diff --git a/src/controller/product.controller.ts b/src/controller/product.controller.ts index bc500cf..9d294d0 100644 --- a/src/controller/product.controller.ts +++ b/src/controller/product.controller.ts @@ -253,21 +253,6 @@ export class ProductController { } } - - // 获取所有 WordPress 商品 - @ApiOkResponse({ description: '获取所有 WordPress 商品' }) - @Get('/wp-products') - async getWpProducts() { - try { - const data = await this.productService.getWpProducts(); - return successResponse(data); - } catch (error) { - return errorResponse(error?.message || error); - } - } - - - // 通用属性接口:分页列表 @ApiOkResponse() @Get('/attribute') diff --git a/src/controller/wp_product.controller.ts b/src/controller/wp_product.controller.ts deleted file mode 100644 index 327c6a2..0000000 --- a/src/controller/wp_product.controller.ts +++ /dev/null @@ -1,232 +0,0 @@ -import { - Controller, - Param, - Post, - Inject, - Get, - Query, - Put, - Body, - Files, - Del, -} from '@midwayjs/core'; -import { WpProductService } from '../service/wp_product.service'; -import { errorResponse, successResponse } from '../utils/response.util'; -import { ApiOkResponse } from '@midwayjs/swagger'; -import { BooleanRes, WpProductListRes } from '../dto/reponse.dto'; -import { - QueryWpProductDTO, - UpdateVariationDTO, - UpdateWpProductDTO, - BatchSyncProductsDTO, - BatchUpdateTagsDTO, - BatchUpdateProductsDTO, -} from '../dto/wp_product.dto'; - -import { - ProductsRes, -} from '../dto/reponse.dto'; -@Controller('/wp_product') -export class WpProductController { - // 移除控制器内的配置站点引用,统一由服务层处理站点数据 - - @Inject() - private readonly wpProductService: WpProductService; - - // 平台服务保留按需注入 - - @ApiOkResponse({ - type: BooleanRes, - }) - @Del('/:id') - async delete(@Param('id') id: number) { - return errorResponse('接口已废弃,请改用 /site-api/:siteId/products 删除'); - } - - @ApiOkResponse({ - type: BooleanRes, - }) - @Post('/import/:siteId') - async importProducts(@Param('siteId') siteId: number, @Files() files) { - try { - if (!files || files.length === 0) { - throw new Error('请上传文件'); - } - await this.wpProductService.importProducts(siteId, files[0]); - return successResponse(true); - } catch (error) { - console.error('导入失败:', error); - return errorResponse(error.message || '导入失败'); - } - } - - @ApiOkResponse({ - type: BooleanRes, - }) - @Post('/setconstitution') - async setConstitution(@Body() body: any) { - try { - return successResponse(true); - } catch (error) { - return errorResponse(error.message || '设置失败'); - } - } - - @ApiOkResponse({ - type: BooleanRes, - }) - @Post('/batch-update') - async batchUpdateProducts(@Body() body: BatchUpdateProductsDTO) { - try { - await this.wpProductService.batchUpdateProducts(body); - return successResponse(true); - } catch (error) { - return errorResponse(error.message || '批量更新失败'); - } - } - - @ApiOkResponse({ - type: BooleanRes, - }) - @Post('/batch-update-tags') - async batchUpdateTags(@Body() body: BatchUpdateTagsDTO) { - try { - await this.wpProductService.batchUpdateTags(body.ids, body.tags); - return successResponse(true); - } catch (error) { - return errorResponse(error.message || '批量更新标签失败'); - } - } - - @ApiOkResponse({ - type: BooleanRes, - }) - @Post('/sync/:siteId') - async syncProducts(@Param('siteId') siteId: number) { - try { - const result = await this.wpProductService.syncSite(siteId); - return successResponse(result); - } catch (error) { - console.log(error); - return errorResponse('同步失败'); - } - } - - @ApiOkResponse({ - type: BooleanRes, - }) - @Post('/batch-sync-to-site/:siteId') - async batchSyncToSite( - @Param('siteId') siteId: number, - @Body() body: BatchSyncProductsDTO - ) { - try { - await this.wpProductService.batchSyncToSite(siteId, body.productIds); - return successResponse(true, '批量同步成功'); - } catch (error) { - console.error('批量同步失败:', error); - return errorResponse(error.message || '批量同步失败'); - } - } - - @ApiOkResponse({ - type: WpProductListRes, - }) - @Get('/list') - async getWpProducts(@Query() query: QueryWpProductDTO) { - return errorResponse('接口已废弃,请改用 /site-api/:siteId/products 列表'); - } - - @ApiOkResponse({ - type: BooleanRes - }) - @Post('/updateState/:id') - async updateWPProductState( - @Param('id') id: number, - @Body() body: any, // todo - ) { - try { - const res = await this.wpProductService.updateProductStatus(id, body?.status, body?.stock_status); - return successResponse(res); - } catch (error) { - return errorResponse(error.message); - } - } - - /** - * 创建产品接口 - * @param siteId 站点 ID - * @param body 创建数据 - */ - @ApiOkResponse({ - type: BooleanRes, - }) - @Post('/siteId/:siteId/products') - async createProduct( - @Param('siteId') siteId: number, - @Body() body: any - ) { - return errorResponse('接口已废弃,请改用 /site-api/:siteId/products 创建'); - } - - /** - * 更新产品接口 - * @param productId 产品 ID - * @param body 更新数据 - */ - @ApiOkResponse({ - type: BooleanRes, - }) - @Put('/siteId/:siteId/products/:productId') - async updateProduct( - @Param('siteId') siteId: number, - @Param('productId') productId: string, - @Body() body: UpdateWpProductDTO - ) { - return errorResponse('接口已废弃,请改用 /site-api/:siteId/products/:id 更新'); - } - - @ApiOkResponse({ - type: BooleanRes, - }) - @Post('/sync-to-product/:id') - async syncToProduct(@Param('id') id: number) { - try { - await this.wpProductService.syncToProduct(id); - return successResponse(true); - } catch (error) { - return errorResponse(error.message); - } - } - - /** - * 更新变体接口 - * @param productId 产品 ID - * @param variationId 变体 ID - * @param body 更新数据 - */ - @Put('/siteId/:siteId/products/:productId/variations/:variationId') - async updateVariation( - @Param('siteId') siteId: number, - @Param('productId') productId: string, - @Param('variationId') variationId: string, - @Body() body: UpdateVariationDTO - ) { - return errorResponse('接口已废弃,请改用 /site-api/:siteId/products/:productId/variations/:variationId 更新'); - } - - @ApiOkResponse({ - description: '通过name搜索产品/订单', - type: ProductsRes, - }) - @Get('/search') - async searchProducts(@Query('name') name: string) { - try { - // 调用服务获取产品数据 - const products = await this.wpProductService.findProductsByName(name); - return successResponse(products); - } catch (error) { - return errorResponse(error.message || '获取数据失败'); - } - } -} diff --git a/src/dto/customer.dto.ts b/src/dto/customer.dto.ts index 8f56b5a..17404cd 100644 --- a/src/dto/customer.dto.ts +++ b/src/dto/customer.dto.ts @@ -59,4 +59,12 @@ export class CustomerDto { @ApiProperty() state: string; +} + +export class CustomerListResponseDTO { + @ApiProperty() + total: number; + + @ApiProperty({ type: [CustomerDto] }) + list: CustomerDto[]; } \ No newline at end of file diff --git a/src/dto/reponse.dto.ts b/src/dto/reponse.dto.ts index 0db709f..4d565e3 100644 --- a/src/dto/reponse.dto.ts +++ b/src/dto/reponse.dto.ts @@ -11,7 +11,6 @@ import { OrderStatusCountDTO } from './order.dto'; import { SiteConfig } from './site.dto'; import { PurchaseOrderDTO, StockDTO, StockRecordDTO } from './stock.dto'; import { LoginResDTO } from './user.dto'; -import { WpProductDTO } from './wp_product.dto'; import { OrderSale } from '../entity/order_sale.entity'; import { Service } from '../entity/service.entity'; import { RateDTO } from './freightcom.dto'; @@ -77,15 +76,6 @@ export class ProductSizeAllRes extends SuccessArrayWrapper(Dict) {} // 产品尺寸返回数据 export class ProductSizeRes extends SuccessWrapper(Dict) {} -//产品分页数据 -export class WpProductPaginatedResponse extends PaginatedWrapper( - WpProductDTO -) {} -//产品分页返回数据 -export class WpProductListRes extends SuccessWrapper( - WpProductPaginatedResponse -) {} - export class LoginRes extends SuccessWrapper(LoginResDTO) {} export class StockPaginatedRespone extends PaginatedWrapper(StockDTO) {} export class StockListRes extends SuccessWrapper(StockPaginatedRespone) {} diff --git a/src/dto/site-api.dto.ts b/src/dto/site-api.dto.ts index 1ebdfa3..c3ccf09 100644 --- a/src/dto/site-api.dto.ts +++ b/src/dto/site-api.dto.ts @@ -14,9 +14,6 @@ export class UnifiedPaginationDTO { @ApiProperty({ description: '每页数量', example: 20 }) per_page: number; - @ApiProperty({ description: '每页数量别名', example: 20 }) - page_size?: number; - @ApiProperty({ description: '总页数', example: 5 }) totalPages: number; } diff --git a/src/dto/woocommerce.dto.ts b/src/dto/woocommerce.dto.ts index 044afe1..12e4ec2 100644 --- a/src/dto/woocommerce.dto.ts +++ b/src/dto/woocommerce.dto.ts @@ -1,6 +1,8 @@ // WooCommerce 平台原始数据类型定义 // 仅包含当前映射逻辑所需字段以保持简洁与类型安全 +import { Variation } from "../entity/variation.entity"; + // 产品类型 export interface WooProduct { // 产品主键 @@ -124,6 +126,9 @@ export interface WooProduct { // 元数据 meta_data?: Array<{ id?: number; key: string; value: any }>; } +export interface WooVariation extends Variation{ + +} // 订单类型 export interface WooOrder { diff --git a/src/dto/wp_product.dto.ts b/src/dto/wp_product.dto.ts deleted file mode 100644 index 48212a3..0000000 --- a/src/dto/wp_product.dto.ts +++ /dev/null @@ -1,136 +0,0 @@ -import { ApiProperty } from '@midwayjs/swagger'; -import { Variation } from '../entity/variation.entity'; -import { WpProduct } from '../entity/wp_product.entity'; -import { Rule, RuleType } from '@midwayjs/validate'; -import { ProductStatus } from '../enums/base.enum'; - -export class VariationDTO extends Variation {} - -export class WpProductDTO extends WpProduct { - @ApiProperty({ description: '变体列表', type: VariationDTO, isArray: true }) - variations?: VariationDTO[]; -} - -export class UpdateVariationDTO { - @ApiProperty({ description: '产品名称' }) - @Rule(RuleType.string().optional()) - name?: string; - - @ApiProperty({ description: 'SKU' }) - @Rule(RuleType.string().allow('').optional()) - sku?: string; - - @ApiProperty({ description: '常规价格', type: Number }) - @Rule(RuleType.number().optional()) - regular_price?: number; // 常规价格 - - @ApiProperty({ description: '销售价格', type: Number }) - @Rule(RuleType.number().optional()) - sale_price?: number; // 销售价格 - - @ApiProperty({ description: '是否促销中', type: Boolean }) - @Rule(RuleType.boolean().optional()) - on_sale?: boolean; // 是否促销中 -} - -export class UpdateWpProductDTO { - @ApiProperty({ description: '变体名称' }) - @Rule(RuleType.string().optional()) - name?: string; - - @ApiProperty({ description: 'SKU' }) - @Rule(RuleType.string().allow('').optional()) - sku?: string; - - @ApiProperty({ description: '常规价格', type: Number }) - @Rule(RuleType.number().optional()) - regular_price?: number; // 常规价格 - - @ApiProperty({ description: '销售价格', type: Number }) - @Rule(RuleType.number().optional()) - sale_price?: number; // 销售价格 - - @ApiProperty({ description: '是否促销中', type: Boolean }) - @Rule(RuleType.boolean().optional()) - on_sale?: boolean; // 是否促销中 - - @ApiProperty({ description: '分类列表', type: [String] }) - @Rule(RuleType.array().items(RuleType.string()).optional()) - categories?: string[]; - - @ApiProperty({ description: '标签列表', type: [String] }) - @Rule(RuleType.array().items(RuleType.string()).optional()) - tags?: string[]; - - @ApiProperty({ description: '站点ID', required: false }) - @Rule(RuleType.number().optional()) - siteId?: number; -} - -export class QueryWpProductDTO { - @ApiProperty({ example: '1', description: '页码' }) - @Rule(RuleType.number()) - current: number; - - @ApiProperty({ example: '10', description: '每页大小' }) - @Rule(RuleType.number()) - pageSize: number; - - @ApiProperty({ example: 'ZYN', description: '产品名' }) - @Rule(RuleType.string()) - name?: string; - - @ApiProperty({ example: '1', description: '站点ID' }) - @Rule(RuleType.string()) - siteId?: string; - - @ApiProperty({ description: '产品状态', enum: ProductStatus }) - @Rule(RuleType.string().valid(...Object.values(ProductStatus))) - status?: ProductStatus; - - @ApiProperty({ description: 'SKU列表', type: Array }) - @Rule(RuleType.array().items(RuleType.string()).single()) - skus?: string[]; -} - -export class BatchSyncProductsDTO { - @ApiProperty({ description: '产品ID列表', type: [Number] }) - @Rule(RuleType.array().items(RuleType.number()).required()) - productIds: number[]; -} - -export class BatchUpdateTagsDTO { - @ApiProperty({ description: '产品ID列表', type: [Number] }) - @Rule(RuleType.array().items(RuleType.number()).required()) - ids: number[]; - - @ApiProperty({ description: '标签列表', type: [String] }) - @Rule(RuleType.array().items(RuleType.string()).required()) - tags: string[]; -} - -export class BatchUpdateProductsDTO { - @ApiProperty({ description: '产品ID列表', type: [Number] }) - @Rule(RuleType.array().items(RuleType.number()).required()) - ids: number[]; - - @ApiProperty({ description: '常规价格', type: Number }) - @Rule(RuleType.number()) - regular_price?: number; - - @ApiProperty({ description: '销售价格', type: Number }) - @Rule(RuleType.number()) - sale_price?: number; - - @ApiProperty({ description: '分类列表', type: [String] }) - @Rule(RuleType.array().items(RuleType.string())) - categories?: string[]; - - @ApiProperty({ description: '标签列表', type: [String] }) - @Rule(RuleType.array().items(RuleType.string())) - tags?: string[]; - - @ApiProperty({ description: '状态', enum: ProductStatus }) - @Rule(RuleType.string().valid(...Object.values(ProductStatus))) - status?: ProductStatus; -} diff --git a/src/entity/product.entity.ts b/src/entity/product.entity.ts index 59dbfb8..d0ec727 100644 --- a/src/entity/product.entity.ts +++ b/src/entity/product.entity.ts @@ -16,7 +16,7 @@ import { ProductStockComponent } from './product_stock_component.entity'; import { ProductSiteSku } from './product_site_sku.entity'; import { Category } from './category.entity'; -@Entity() +@Entity('product_v2') export class Product { @ApiProperty({ example: '1', diff --git a/src/entity/product_site_sku.entity.ts b/src/entity/product_site_sku.entity.ts index c91c172..f6b3c40 100644 --- a/src/entity/product_site_sku.entity.ts +++ b/src/entity/product_site_sku.entity.ts @@ -10,7 +10,7 @@ import { import { ApiProperty } from '@midwayjs/swagger'; import { Product } from './product.entity'; -@Entity('product_site_sku') +@Entity('product_site_sku') export class ProductSiteSku { @PrimaryGeneratedColumn() id: number; diff --git a/src/service/customer.service.ts b/src/service/customer.service.ts index 1b9cf00..c23f260 100644 --- a/src/service/customer.service.ts +++ b/src/service/customer.service.ts @@ -5,7 +5,7 @@ import { Repository } from 'typeorm'; import { CustomerTag } from '../entity/customer_tag.entity'; import { Customer } from '../entity/customer.entity'; import { SiteApiService } from './site-api.service'; -import { UnifiedCustomerDTO, UnifiedSearchParamsDTO } from '../dto/site-api.dto'; +import { UnifiedCustomerDTO, UnifiedPaginationDTO, UnifiedSearchParamsDTO } from '../dto/site-api.dto'; import { SyncOperationResult, BatchErrorItem } from '../dto/batch.dto'; @Provide() @@ -332,7 +332,7 @@ export class CustomerService { * 支持基本的分页、搜索和排序功能 * 使用TypeORM查询构建器实现 */ - async getCustomerList(param: Record): Promise{ + async getCustomerList(param: Record): Promise>{ const { current = 1, pageSize = 10, @@ -427,8 +427,9 @@ export class CustomerService { return { items: processedItems, total, - current, - pageSize, + page: current, + per_page: pageSize, + totalPages: Math.ceil(total / pageSize), }; } diff --git a/src/service/order.service.ts b/src/service/order.service.ts index 466e714..fe7564e 100644 --- a/src/service/order.service.ts +++ b/src/service/order.service.ts @@ -7,7 +7,6 @@ import { plainToClass } from 'class-transformer'; import { OrderItem } from '../entity/order_item.entity'; import { OrderSale } from '../entity/order_sale.entity'; -import { WpProduct } from '../entity/wp_product.entity'; import { Product } from '../entity/product.entity'; import { OrderFee } from '../entity/order_fee.entity'; import { OrderRefund } from '../entity/order_refund.entity'; @@ -57,10 +56,6 @@ export class OrderService { @InjectEntityModel(OrderSale) orderSaleModel: Repository; - - @InjectEntityModel(WpProduct) - wpProductModel: Repository; - @InjectEntityModel(Variation) variationModel: Repository; @@ -1621,86 +1616,84 @@ export class OrderService { //换货确认按钮改成调用这个方法 //换货功能更新OrderSale和Orderitem数据 async updateExchangeOrder(orderId: number, data: any) { - try { - const dataSource = this.dataSourceManager.getDataSource('default'); - let transactionError = undefined; + 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); + // await dataSource.transaction(async manager => { + // const orderRepo = manager.getRepository(Order); + // const orderSaleRepo = manager.getRepository(OrderSale); + // const orderItemRepo = manager.getRepository(OrderItem); - const productRepo = manager.getRepository(Product); - const WpProductRepo = manager.getRepository(WpProduct); + // const productRepo = manager.getRepository(ProductV2); - const order = await orderRepo.findOneBy({ id: orderId }); - let product: Product; - let wpProduct: WpProduct; + // const order = await orderRepo.findOneBy({ id: orderId }); + // let product: ProductV2; - await orderSaleRepo.delete({ orderId }); - await orderItemRepo.delete({ orderId }); - for (const sale of data['sales']) { - product = await productRepo.findOneBy({ sku: sale['sku'] }); - await orderSaleRepo.save({ - orderId, - siteId: order.siteId, - productId: product.id, - name: product.name, - sku: sale['sku'], - quantity: sale['quantity'], - }); - }; + // await orderSaleRepo.delete({ orderId }); + // await orderItemRepo.delete({ orderId }); + // for (const sale of data['sales']) { + // product = await productRepo.findOneBy({ sku: sale['sku'] }); + // await orderSaleRepo.save({ + // orderId, + // siteId: order.siteId, + // productId: product.id, + // name: product.name, + // sku: sale['sku'], + // quantity: sale['quantity'], + // }); + // }; - for (const item of data['items']) { - wpProduct = await WpProductRepo.findOneBy({ sku: item['sku'] }); + // for (const item of data['items']) { + // product = await productRepo.findOneBy({ sku: item['sku'] }); + // await orderItemRepo.save({ + // orderId, + // siteId: order.siteId, + // productId: product.id, + // name: product.name, + // externalOrderId: order.externalOrderId, + // externalProductId: product.externalProductId, - await orderItemRepo.save({ - orderId, - siteId: order.siteId, - productId: wpProduct.id, - name: wpProduct.name, - externalOrderId: order.externalOrderId, - externalProductId: wpProduct.externalProductId, + // sku: item['sku'], + // quantity: item['quantity'], + // }); - sku: item['sku'], - quantity: item['quantity'], - }); + // }; - }; + // //将是否换货状态改为true + // await orderRepo.update( + // order.id + // , { + // is_exchange: true + // }); - //将是否换货状态改为true - await orderRepo.update( - order.id - , { - is_exchange: true - }); + // //查询这个用户换过多少次货 + // const counts = await orderRepo.countBy({ + // is_editable: true, + // customer_email: order.customer_email, + // }); - //查询这个用户换过多少次货 - const counts = await orderRepo.countBy({ - is_editable: true, - customer_email: order.customer_email, - }); + // //批量更新当前用户换货次数 + // await orderRepo.update({ + // customer_email: order.customer_email + // }, { + // exchange_frequency: counts + // }); - //批量更新当前用户换货次数 - await orderRepo.update({ - customer_email: order.customer_email - }, { - exchange_frequency: counts - }); + // }).catch(error => { + // transactionError = error; + // }); - }).catch(error => { - transactionError = error; - }); - - if (transactionError !== undefined) { - throw new Error(`更新物流信息错误:${transactionError.message}`); - } - return true; - } catch (error) { - throw new Error(`更新发货产品失败:${error.message}`); - } + // if (transactionError !== undefined) { + // throw new Error(`更新物流信息错误:${transactionError.message}`); + // } + // return true; + // } catch (error) { + // throw new Error(`更新发货产品失败:${error.message}`); + // } } } diff --git a/src/service/product.service.ts b/src/service/product.service.ts index ccc12f6..40e5b5f 100644 --- a/src/service/product.service.ts +++ b/src/service/product.service.ts @@ -19,7 +19,6 @@ import { SizePaginatedResponse, } from '../dto/reponse.dto'; import { InjectEntityModel } from '@midwayjs/typeorm'; -import { WpProduct } from '../entity/wp_product.entity'; import { Variation } from '../entity/variation.entity'; import { Dict } from '../entity/dict.entity'; import { DictItem } from '../entity/dict_item.entity'; @@ -53,9 +52,6 @@ export class ProductService { @InjectEntityModel(DictItem) dictItemModel: Repository; - @InjectEntityModel(WpProduct) - wpProductModel: Repository; - @InjectEntityModel(Variation) variationModel: Repository; @@ -74,11 +70,6 @@ export class ProductService { @InjectEntityModel(Category) categoryModel: Repository; - - // 获取所有 WordPress 商品 - async getWpProducts() { - return this.wpProductModel.find(); - } // 获取所有分类 async getCategoriesAll(): Promise { return this.categoryModel.find({ diff --git a/src/service/wp.service.ts b/src/service/wp.service.ts index f3e35b7..c99e66c 100644 --- a/src/service/wp.service.ts +++ b/src/service/wp.service.ts @@ -5,14 +5,13 @@ import { Inject, Provide } from '@midwayjs/core'; import axios, { AxiosRequestConfig } from 'axios'; import WooCommerceRestApi, { WooCommerceRestApiVersion } from '@woocommerce/woocommerce-rest-api'; -import { WpProduct } from '../entity/wp_product.entity'; import { Variation } from '../entity/variation.entity'; -import { UpdateVariationDTO, UpdateWpProductDTO } from '../dto/wp_product.dto'; import { SiteService } from './site.service'; 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'; const MAX_PAGE_SIZE = 100; @Provide() export class WPService implements IPlatformService { @@ -244,7 +243,7 @@ export class WPService implements IPlatformService { async getProducts(site: any, page: number = 1, pageSize: number = 100): Promise { const api = this.createApi(site, 'wc/v3'); - return await this.sdkGetPage(api, 'products', { page, per_page: pageSize }); + return await this.sdkGetPage(api, 'products', { page, per_page: pageSize }); } async getProduct(site: any, id: number): Promise { @@ -393,7 +392,7 @@ export class WPService implements IPlatformService { async updateProduct( site: any, productId: string, - data: UpdateWpProductDTO + data: WooProduct ): Promise { const { regular_price, sale_price, ...params } = data; const api = this.createApi(site, 'wc/v3'); @@ -510,7 +509,7 @@ export class WPService implements IPlatformService { site: any, productId: string, variationId: string, - data: Partial + data: Partial ): Promise { const { regular_price, sale_price, ...params } = data; const api = this.createApi(site, 'wc/v3'); diff --git a/src/service/wp_product.service.ts b/src/service/wp_product.service.ts deleted file mode 100644 index 6b900e2..0000000 --- a/src/service/wp_product.service.ts +++ /dev/null @@ -1,1214 +0,0 @@ -import { ProductSiteSku } from '../entity/product_site_sku.entity'; -import { Product } from '../entity/product.entity'; -import { Inject, Provide } from '@midwayjs/core'; -import * as fs from 'fs'; -import { parse } from 'csv-parse'; -import { WPService } from './wp.service'; -import { WpProduct } from '../entity/wp_product.entity'; -import { InjectEntityModel } from '@midwayjs/typeorm'; -import { And, Like, Not, Repository, In } from 'typeorm'; -import { Variation } from '../entity/variation.entity'; -import { - QueryWpProductDTO, - UpdateVariationDTO, - UpdateWpProductDTO, - BatchUpdateProductsDTO, -} from '../dto/wp_product.dto'; -import { ProductStatus, ProductStockStatus } from '../enums/base.enum'; -import { SiteService } from './site.service'; - -import { StockService } from './stock.service'; - -@Provide() -export class WpProductService { - // 移除配置中的站点数组,统一从数据库获取站点信息 - - @Inject() - private readonly wpApiService: WPService; - - @Inject() - private readonly siteService: SiteService; - - @Inject() - private readonly stockService: StockService; - - @InjectEntityModel(WpProduct) - wpProductModel: Repository; - - @InjectEntityModel(Variation) - variationModel: Repository; - - @InjectEntityModel(Product) - productModel: Repository; - - @InjectEntityModel(ProductSiteSku) - productSiteSkuModel: Repository; - - - async syncAllSites() { - // 从数据库获取所有启用的站点,并逐站点同步产品与变体 - const { items: sites } = await this.siteService.list({ current: 1, pageSize: Infinity, isDisabled: false }, true); - for (const site of sites) { - const products = await this.wpApiService.getProducts(site); - for (const product of products) { - const variations = - product.type === 'variable' - ? await this.wpApiService.getVariations(site, product.id) - : []; - await this.syncProductAndVariations(site.id, product, variations); - } - } - } - - private logToFile(msg: string, data?: any) { - const logFile = '/Users/zksu/Developer/work/workcode/API/debug_sync.log'; - const timestamp = new Date().toISOString(); - let content = `[${timestamp}] ${msg}`; - if (data !== undefined) { - content += ' ' + (typeof data === 'object' ? JSON.stringify(data) : String(data)); - } - content += '\n'; - try { - fs.appendFileSync(logFile, content); - } catch (e) { - console.error('Failed to write to log file:', e); - } - console.log(msg, data || ''); - } - - async batchSyncToSite(siteId: number, productIds: number[]) { - this.logToFile(`[BatchSync] Starting sync to site ${siteId} for products:`, productIds); - const site = await this.siteService.get(siteId, true); - const products = await this.productModel.find({ - where: { id: In(productIds) }, - }); - this.logToFile(`[BatchSync] Found ${products.length} products in local DB`); - - const batchData = { - create: [], - update: [], - }; - - const skuToProductMap = new Map(); - - for (const product of products) { - const targetSku = (site.skuPrefix || '') + product.sku; - skuToProductMap.set(targetSku, product); - - // Determine if we should create or update based on local WpProduct record - const existingWpProduct = await this.wpProductModel.findOne({ - where: { siteId, sku: targetSku, on_delete: false } - }); - - const productData = { - name: product.name, - type: product.type === 'single' ? 'simple' : (product.type === 'bundle' ? 'bundle' : 'simple'), - regular_price: product.price ? String(product.price) : '0', - sale_price: product.promotionPrice ? String(product.promotionPrice) : '', - sku: targetSku, - status: 'publish', // Default to publish - // categories? - }; - - if (existingWpProduct) { - batchData.update.push({ - id: existingWpProduct.externalProductId, - ...productData - }); - } else { - batchData.create.push(productData); - } - } - - this.logToFile(`[BatchSync] Payload - Create: ${batchData.create.length}, Update: ${batchData.update.length}`); - if (batchData.create.length > 0) this.logToFile('[BatchSync] Create Payload:', JSON.stringify(batchData.create)); - if (batchData.update.length > 0) this.logToFile('[BatchSync] Update Payload:', JSON.stringify(batchData.update)); - - if (batchData.create.length === 0 && batchData.update.length === 0) { - this.logToFile('[BatchSync] No actions needed, skipping API call'); - return; - } - - let result; - try { - result = await this.wpApiService.batchProcessProducts(site, batchData); - this.logToFile('[BatchSync] API Success. Result:', JSON.stringify(result)); - } catch (error) { - this.logToFile('[BatchSync] API Error:', error); - throw error; - } - - // Process results to update local WpProduct and ProductSiteSku - const processResultItem = async (item: any, sourceList: any[], index: number) => { - const originalSku = sourceList[index]?.sku; - - if (item.id) { - this.logToFile(`[BatchSync] Processing success item: ID=${item.id}, SKU=${item.sku}`); - let localProduct = skuToProductMap.get(item.sku); - - // Fallback to original SKU if response SKU differs or lookup fails - if (!localProduct && originalSku) { - localProduct = skuToProductMap.get(originalSku); - } - - if (localProduct) { - this.logToFile(`[BatchSync] Found local product ID=${localProduct.id} for SKU=${item.sku || originalSku}`); - const code = item.sku || originalSku; - const existingSiteSku = await this.productSiteSkuModel.findOne({ - where: { productId: localProduct.id, siteSku: code }, - }); - if (!existingSiteSku) { - this.logToFile(`[BatchSync] Creating ProductSiteSku for productId=${localProduct.id} code=${code}`); - await this.productSiteSkuModel.save({ - productId: localProduct.id, - siteSku: code, - }); - } else { - this.logToFile(`[BatchSync] ProductSiteSku already exists for productId=${localProduct.id} code=${code}`); - } - } else { - this.logToFile(`[BatchSync] Warning: Local product not found in map for SKU=${item.sku || originalSku}`); - } - - // Sync back to local WpProduct table - await this.syncProductAndVariations(siteId, item, []); - } else if (item.error) { - // Handle duplicated SKU error by linking to existing remote product - if (item.error.code === 'product_invalid_sku' && item.error.data && item.error.data.resource_id) { - const recoveredSku = item.error.data.unique_sku; - const resourceId = item.error.data.resource_id; - this.logToFile(`[BatchSync] Recovering from duplicate SKU error. SKU=${recoveredSku}, ID=${resourceId}`); - - let localProduct = skuToProductMap.get(recoveredSku); - - // Fallback to original SKU - if (!localProduct && originalSku) { - localProduct = skuToProductMap.get(originalSku); - } - - if (localProduct) { - // Construct a fake product object to sync local DB - const fakeProduct = { - id: resourceId, - sku: recoveredSku, // Use the actual SKU on server - name: localProduct.name, - type: localProduct.type === 'single' ? 'simple' : (localProduct.type === 'bundle' ? 'bundle' : 'simple'), - status: 'publish', - regular_price: localProduct.price ? String(localProduct.price) : '0', - sale_price: localProduct.promotionPrice ? String(localProduct.promotionPrice) : '', - on_sale: !!localProduct.promotionPrice, - metadata: [], - tags: [], - categories: [] - }; - - try { - await this.syncProductAndVariations(siteId, fakeProduct as any, []); - this.logToFile(`[BatchSync] Successfully linked local product to existing remote product ID=${resourceId}`); - } catch (e) { - this.logToFile(`[BatchSync] Failed to link recovered product:`, e); - } - } else { - this.logToFile(`[BatchSync] Warning: Local product not found in map for recovered SKU=${recoveredSku} or original SKU=${originalSku}`); - } - } else { - this.logToFile(`[BatchSync] Item Error: SKU=${originalSku || 'unknown'}`, item.error); - } - } else { - this.logToFile(`[BatchSync] Unknown item format:`, item); - } - }; - - if (result.create) { - for (let i = 0; i < result.create.length; i++) { - await processResultItem(result.create[i], batchData.create, i); - } - } - - if (result.update) { - for (let i = 0; i < result.update.length; i++) { - await processResultItem(result.update[i], batchData.update, i); - } - } - - return result; - } - - async batchUpdateTags(ids: number[], tags: string[]) { - if (!ids || ids.length === 0 || !tags || tags.length === 0) return; - - const products = await this.wpProductModel.find({ - where: { id: In(ids) }, - }); - - // Group by siteId - const productsBySite = new Map(); - for (const product of products) { - if (!productsBySite.has(product.siteId)) { - productsBySite.set(product.siteId, []); - } - productsBySite.get(product.siteId).push(product); - } - - for (const [siteId, siteProducts] of productsBySite) { - const site = await this.siteService.get(siteId, true); - if (!site) continue; - - const batchData = { - create: [], - update: [], - }; - - for (const product of siteProducts) { - const currentTags = product.tags || []; - // Add new tags, avoiding duplicates by name - const newTags = [...currentTags]; - const tagsToAdd = []; - - for (const tag of tags) { - if (!newTags.some(t => t.name === tag)) { - const newTagObj = { name: tag, id: 0, slug: '' }; - newTags.push(newTagObj); - tagsToAdd.push(newTagObj); - } - } - - if (tagsToAdd.length > 0) { - batchData.update.push({ - id: product.externalProductId, - tags: newTags.map(t => (t.id ? { id: t.id } : { name: t.name })), - }); - // Update local DB optimistically - // Generate slug simply - tagsToAdd.forEach(t => (t.slug = t.name.toLowerCase().replace(/\s+/g, '-'))); - product.tags = newTags; - await this.wpProductModel.save(product); - } - } - - if (batchData.update.length > 0) { - await this.wpApiService.batchProcessProducts(site, batchData); - } - } - } - - async batchUpdateProducts(dto: BatchUpdateProductsDTO) { - const { ids, ...updates } = dto; - if (!ids || ids.length === 0) return; - - const products = await this.wpProductModel.find({ - where: { id: In(ids) }, - }); - - // Group by siteId - const productsBySite = new Map(); - for (const product of products) { - if (!productsBySite.has(product.siteId)) { - productsBySite.set(product.siteId, []); - } - productsBySite.get(product.siteId).push(product); - } - - for (const [siteId, siteProducts] of productsBySite) { - const site = await this.siteService.get(siteId, true); - if (!site) continue; - - // Resolve Categories if needed - let categoryIds: number[] = []; - if (updates.categories && updates.categories.length > 0) { - // 1. Get all existing categories - const allCategories = await this.wpApiService.getCategories(site); - const existingCatMap = new Map(allCategories.map(c => [c.name, c.id])); - - // 2. Identify missing categories - const missingCategories = updates.categories.filter(name => !existingCatMap.has(name)); - - // 3. Create missing categories - if (missingCategories.length > 0) { - const createPayload = missingCategories.map(name => ({ name })); - const createdCatsResult = await this.wpApiService.batchProcessCategories(site, { create: createPayload }); - if (createdCatsResult && createdCatsResult.create) { - createdCatsResult.create.forEach(c => { - if (c.id && c.name) existingCatMap.set(c.name, c.id); - }); - } - } - - // 4. Collect all IDs - categoryIds = updates.categories - .map(name => existingCatMap.get(name)) - .filter(id => id !== undefined); - } - - // Resolve Tags if needed - let tagIds: number[] = []; - if (updates.tags && updates.tags.length > 0) { - // 1. Get all existing tags - const allTags = await this.wpApiService.getTags(site); - const existingTagMap = new Map(allTags.map(t => [t.name, t.id])); - - // 2. Identify missing tags - const missingTags = updates.tags.filter(name => !existingTagMap.has(name)); - - // 3. Create missing tags - if (missingTags.length > 0) { - const createPayload = missingTags.map(name => ({ name })); - const createdTagsResult = await this.wpApiService.batchProcessTags(site, { create: createPayload }); - if (createdTagsResult && createdTagsResult.create) { - createdTagsResult.create.forEach(t => { - if (t.id && t.name) existingTagMap.set(t.name, t.id); - }); - } - } - - // 4. Collect all IDs - tagIds = updates.tags - .map(name => existingTagMap.get(name)) - .filter(id => id !== undefined); - } - - const batchData = { - create: [], - update: [], - }; - - for (const product of siteProducts) { - const updateData: any = { - id: product.externalProductId, - }; - - if (updates.regular_price) updateData.regular_price = String(updates.regular_price); - if (updates.sale_price) updateData.sale_price = String(updates.sale_price); - if (updates.status) updateData.status = updates.status; - - if (categoryIds.length > 0) { - updateData.categories = categoryIds.map(id => ({ id })); - } - - if (tagIds.length > 0) { - updateData.tags = tagIds.map(id => ({ id })); - } - - batchData.update.push(updateData); - - // Optimistic update local DB - if (updates.regular_price) product.regular_price = updates.regular_price; - if (updates.sale_price) product.sale_price = updates.sale_price; - if (updates.status) product.status = updates.status as ProductStatus; - if (updates.categories) product.categories = updates.categories.map(c => ({ name: c, id: 0, slug: '' })); // simple mock - if (updates.tags) product.tags = updates.tags.map(t => ({ name: t, id: 0, slug: '' })); // simple mock - - await this.wpProductModel.save(product); - } - - if (batchData.update.length > 0) { - await this.wpApiService.batchProcessProducts(site, batchData); - } - } - } - - async importProducts(siteId: number, file: any) { - const site = await this.siteService.get(siteId, true); - if (!site) throw new Error('站点不存在'); - - const parser = fs - .createReadStream(file.data) - .pipe(parse({ - columns: true, - skip_empty_lines: true, - trim: true, - bom: true - })); - - let batch = []; - const batchSize = 50; - - for await (const record of parser) { - batch.push(record); - if (batch.length >= batchSize) { - await this.processImportBatch(siteId, site, batch); - batch = []; - } - } - - if (batch.length > 0) { - await this.processImportBatch(siteId, site, batch); - } - } - - private async processImportBatch(siteId: number, site: any, chunk: any[]) { - const batchData = { - create: [], - update: [], - }; - - for (const row of chunk) { - const sku = row['SKU'] || row['sku']; - if (!sku) continue; - - const existingProduct = await this.wpProductModel.findOne({ - where: { siteId, sku } - }); - - const productData: any = { - sku: sku, - name: row['Name'] || row['name'], - type: (row['Type'] || row['type'] || 'simple').toLowerCase(), - regular_price: row['Regular price'] || row['regular_price'], - sale_price: row['Sale price'] || row['sale_price'], - short_description: row['Short description'] || row['short_description'] || '', - description: row['Description'] || row['description'] || '', - }; - - if (productData.regular_price) productData.regular_price = String(productData.regular_price); - if (productData.sale_price) productData.sale_price = String(productData.sale_price); - - const imagesStr = row['Images'] || row['images']; - if (imagesStr) { - productData.images = imagesStr.split(',').map(url => ({ src: url.trim() })); - } - - if (existingProduct) { - batchData.update.push({ - id: existingProduct.externalProductId, - ...productData - }); - } else { - batchData.create.push(productData); - } - } - - if (batchData.create.length > 0 || batchData.update.length > 0) { - try { - const result = await this.wpApiService.batchProcessProducts(site, batchData); - await this.syncBackFromBatchResult(siteId, result); - } catch (e) { - console.error('Batch process error during import:', e); - } - } - } - - private async syncBackFromBatchResult(siteId: number, result: any) { - const processResultItem = async (item: any) => { - if (item.id) { - await this.syncProductAndVariations(siteId, item, []); - } - }; - - if (result.create) { - for (const item of result.create) { - await processResultItem(item); - } - } - if (result.update) { - for (const item of result.update) { - await processResultItem(item); - } - } - } - - - - // 同步产品库存到 Site - async syncProductStockToSite(siteId: number, sku: string) { - const site = await this.siteService.get(siteId, true); - if (!site) throw new Error('站点不存在'); - - // 获取站点绑定的仓库 - if (!site.stockPoints || site.stockPoints.length === 0) { - console.log(`站点 ${siteId} 未绑定任何仓库,跳过库存同步`); - return; - } - - // 获取产品在这些仓库的总库存 - const stockPointIds = site.stockPoints.map(sp => sp.id); - const stock = await this.stockService.stockModel - .createQueryBuilder('stock') - .select('SUM(stock.quantity)', 'total') - .where('stock.sku = :sku', { sku }) - .andWhere('stock.stockPointId IN (:...stockPointIds)', { stockPointIds }) - .getRawOne(); - - const quantity = stock && stock.total ? Number(stock.total) : 0; - const stockStatus = quantity > 0 ? ProductStockStatus.INSTOCK : ProductStockStatus.OUT_OF_STOCK; - - // 查找对应的 WpProduct 以获取 externalProductId - const wpProduct = await this.wpProductModel.findOne({ where: { siteId, sku } }); - if (wpProduct) { - // 更新 WooCommerce 库存 - await this.wpApiService.updateProductStock(site, wpProduct.externalProductId, quantity, stockStatus); - - // 更新本地 WpProduct 状态 - wpProduct.stock_quantity = quantity; - wpProduct.stockStatus = stockStatus; - await this.wpProductModel.save(wpProduct); - } else { - // 尝试查找变体 - const variation = await this.variationModel.findOne({ where: { siteId, sku } }); - if (variation) { - await this.wpApiService.updateProductVariationStock(site, variation.externalProductId, variation.externalVariationId, quantity, stockStatus); - // 变体表目前没有 stock_quantity 字段,如果需要可以添加 - } - } - } - - // 同步一个网站 - async syncSite(siteId: number) { - try { - // 通过数据库获取站点并转换为 Site,用于后续 WooCommerce 同步 - const site = await this.siteService.get(siteId, true); - const externalProductIds = this.wpProductModel.createQueryBuilder('wp_product') - .select([ - 'wp_product.id ', - 'wp_product.externalProductId ', - ]) - .where('wp_product.siteId = :siteId', { - siteId, - }) - const rawResult = await externalProductIds.getRawMany(); - - const externalIds = rawResult.map(item => item.externalProductId); - - const excludeValues = []; - - const products = await this.wpApiService.getProducts(site); - let successCount = 0; - let failureCount = 0; - for (const product of products) { - try { - excludeValues.push(String(product.id)); - const variations = - product.type === 'variable' - ? await this.wpApiService.getVariations(site, product.id) - : []; - - await this.syncProductAndVariations(site.id, product, variations); - successCount++; - } catch (error) { - console.error(`同步产品 ${product.id} 失败:`, error); - failureCount++; - } - } - - const filteredIds = externalIds.filter(id => !excludeValues.includes(id)); - if (filteredIds.length != 0) { - await this.variationModel.createQueryBuilder('variation') - .update() - .set({ on_delete: true }) - .where('variation.siteId = :siteId AND variation.externalProductId IN (:...filteredId)', { siteId, filteredId: filteredIds }) - .execute(); - - this.wpProductModel.createQueryBuilder('wp_product') - .update() - .set({ on_delete: true }) - .where('wp_product.siteId = :siteId AND wp_product.externalProductId IN (:...filteredId)', { siteId, filteredId: filteredIds }) - .execute(); - } - return { - success: failureCount === 0, - successCount, - failureCount, - message: `同步完成: 成功 ${successCount}, 失败 ${failureCount}`, - }; - } catch (error) { - console.error('同步站点产品失败:', error); - return { success: false, successCount: 0, failureCount: 0, message: `同步失败: ${error.message}` }; - } - } - - // 控制产品上下架 - async updateProductStatus(id: number, status: ProductStatus, stock_status: ProductStockStatus) { - const wpProduct = await this.wpProductModel.findOneBy({ id }); - const site = await this.siteService.get(wpProduct.siteId, true); - wpProduct.status = status; - wpProduct.stockStatus = stock_status; - const res = await this.wpApiService.updateProductStatus(site, wpProduct.externalProductId, status, stock_status); - if (res === true) { - this.wpProductModel.save(wpProduct); - return true; - } else { - return res; - } - } - - async findProduct( - siteId: number, - externalProductId: string - ): Promise { - return await this.wpProductModel.findOne({ - where: { siteId, externalProductId }, - }); - } - - async findVariation( - siteId: number, - externalProductId: string, - externalVariationId: string - ): Promise { - return await this.variationModel.findOne({ - where: { siteId, externalProductId, externalVariationId, on_delete: false }, - }); - } - - async updateWpProduct( - siteId: number, - productId: string, - product: UpdateWpProductDTO - ) { - let existingProduct = await this.findProduct(siteId, productId); - if (existingProduct) { - if (product.name) existingProduct.name = product.name; - if (product.sku !== undefined) existingProduct.sku = product.sku; - if (product.regular_price !== undefined && product.regular_price !== null) { - existingProduct.regular_price = product.regular_price; - } - if (product.sale_price !== undefined && product.sale_price !== null) { - existingProduct.sale_price = product.sale_price; - } - if (product.on_sale !== undefined) { - existingProduct.on_sale = product.on_sale; - } - if (product.tags) { - existingProduct.tags = product.tags as any; - } - if (product.categories) { - existingProduct.categories = product.categories as any; - } - await this.wpProductModel.save(existingProduct); - } - } - - async updateWpProductVaritation( - siteId: number, - productId: string, - variationId: string, - variation: UpdateVariationDTO - ) { - const existingVariation = await this.findVariation( - siteId, - productId, - variationId - ); - - if (existingVariation) { - existingVariation.name = variation.name; - existingVariation.sku = variation.sku; - if (variation.regular_price !== undefined && variation.regular_price !== null) { - existingVariation.regular_price = variation.regular_price; - } - if (variation.sale_price !== undefined && variation.sale_price !== null) { - existingVariation.sale_price = variation.sale_price; - } - await this.variationModel.save(existingVariation); - } - } - - async syncProductAndVariations( - siteId: number, - product: WpProduct, - variations: Variation[] - ) { - // 1. 处理产品同步 - let existingProduct = await this.findProduct(siteId, String(product.id)); - - if (existingProduct) { - existingProduct.name = product.name; - existingProduct.status = product.status; - existingProduct.type = product.type; - existingProduct.sku = product.sku; - if (product.regular_price !== undefined && product.regular_price !== null && String(product.regular_price) !== '') { - existingProduct.regular_price = Number(product.regular_price); - } - if (product.sale_price !== undefined && product.sale_price !== null && String(product.sale_price) !== '') { - existingProduct.sale_price = Number(product.sale_price); - } - existingProduct.on_sale = product.on_sale; - existingProduct.metadata = product.metadata; - existingProduct.tags = product.tags; - existingProduct.categories = product.categories; - await this.wpProductModel.save(existingProduct); - } else { - existingProduct = this.wpProductModel.create({ - siteId, - externalProductId: String(product.id), - sku: product.sku, - status: product.status, - name: product.name, - type: product.type, - ...(product.regular_price - ? { regular_price: Number(product.regular_price) } - : {}), - ...(product.sale_price ? { sale_price: Number(product.sale_price) } : {}), - on_sale: product.on_sale, - metadata: product.metadata, - tags: product.tags, - categories: product.categories, - }); - await this.wpProductModel.save(existingProduct); - } - - await this.ensureSiteSku(product.sku, siteId, product.type); - - // 2. 处理变体同步 - if (product.type === 'variable') { - const currentVariations = await this.variationModel.find({ - where: { siteId, externalProductId: String(product.id), on_delete: false }, - }); - const syncedVariationIds = new Set(variations.map(v => String(v.id))); - const variationsToDelete = currentVariations.filter( - dbVariation => - !syncedVariationIds.has(String(dbVariation.externalVariationId)) - ); - if (variationsToDelete.length > 0) { - const idsToDelete = variationsToDelete.map(v => v.id); - await this.variationModel.delete(idsToDelete); - } - - for (const variation of variations) { - await this.ensureSiteSku(variation.sku, siteId); - const existingVariation = await this.findVariation( - siteId, - String(product.id), - String(variation.id) - ); - - if (existingVariation) { - existingVariation.name = variation.name; - existingVariation.attributes = variation.attributes; - variation.regular_price && - (existingVariation.regular_price = variation.regular_price); - variation.sale_price && - (existingVariation.sale_price = variation.sale_price); - existingVariation.on_sale = variation.on_sale; - await this.variationModel.save(existingVariation); - } else { - const newVariation = this.variationModel.create({ - siteId, - externalProductId: String(product.id), - externalVariationId: String(variation.id), - productId: existingProduct.id, - sku: variation.sku, - name: variation.name, - ...(variation.regular_price - ? { regular_price: variation.regular_price } - : {}), - ...(variation.sale_price - ? { sale_price: variation.sale_price } - : {}), - on_sale: variation.on_sale, - attributes: variation.attributes, - }); - await this.variationModel.save(newVariation); - } - } - } else { - // 清理之前的变体 - await this.variationModel.update( - { siteId, externalProductId: String(product.id) }, - { on_delete: true } - ); - } - } - - async syncVariation(siteId: number, productId: string, variation: Variation) { - await this.ensureSiteSku(variation.sku, siteId); - let existingProduct = await this.findProduct(siteId, String(productId)); - if (!existingProduct) return; - const existingVariation = await this.variationModel.findOne({ - where: { - siteId, - externalProductId: String(productId), - externalVariationId: String(variation.id), - }, - }); - - if (existingVariation) { - existingVariation.name = variation.name; - existingVariation.attributes = variation.attributes; - variation.regular_price && - (existingVariation.regular_price = variation.regular_price); - variation.sale_price && - (existingVariation.sale_price = variation.sale_price); - existingVariation.on_sale = variation.on_sale; - await this.variationModel.save(existingVariation); - } else { - const newVariation = this.variationModel.create({ - siteId, - externalProductId: String(productId), - externalVariationId: String(variation.id), - productId: existingProduct.id, - sku: variation.sku, - name: variation.name, - ...(variation.regular_price - ? { regular_price: variation.regular_price } - : {}), - ...(variation.sale_price ? { sale_price: variation.sale_price } : {}), - on_sale: variation.on_sale, - attributes: variation.attributes, - }); - await this.variationModel.save(newVariation); - } - } - - async getProductList(param: QueryWpProductDTO) { - const { current = 1, pageSize = 10, name, siteId, status, skus } = param; - // 第一步:先查询分页的产品 - const where: any = {}; - if (siteId) { - where.siteId = siteId; - } - const nameFilter = name ? name.split(' ').filter(Boolean) : []; - if (nameFilter.length > 0) { - const nameConditions = nameFilter.map(word => Like(`%${word}%`)); - where.name = And(...nameConditions); - } - if (status) { - where.status = status; - } - - if (skus && skus.length > 0) { - // 查找 WpProduct 中匹配的 SKU - const wpProducts = await this.wpProductModel.find({ - select: ['id'], - where: { sku: In(skus), on_delete: false }, - }); - let ids = wpProducts.map(p => p.id); - - // 查找 Variation 中匹配的 SKU,并获取对应的 WpProduct - const variations = await this.variationModel.find({ - select: ['siteId', 'externalProductId'], - where: { sku: In(skus), on_delete: false }, - }); - - if (variations.length > 0) { - const variationParentConditions = variations.map(v => ({ - siteId: v.siteId, - externalProductId: v.externalProductId, - on_delete: false - })); - - // 这里不能直接用 In,因为是 siteId 和 externalProductId 的组合键 - // 可以用 OR 条件查询对应的 WpProduct ID - // 或者,更简单的是,如果我们能获取到 ids... - // 既然 variationParentConditions 可能是多个,我们可以分批查或者构造查询 - - // 使用 QueryBuilder 查 ID - if (variationParentConditions.length > 0) { - const qb = this.wpProductModel.createQueryBuilder('wp_product') - .select('wp_product.id'); - - qb.where('1=0'); // Start with false - - variationParentConditions.forEach((cond, index) => { - qb.orWhere(`(wp_product.siteId = :siteId${index} AND wp_product.externalProductId = :epid${index} AND wp_product.on_delete = :del${index})`, { - [`siteId${index}`]: cond.siteId, - [`epid${index}`]: cond.externalProductId, - [`del${index}`]: false - }); - }); - - const parentProducts = await qb.getMany(); - ids = [...ids, ...parentProducts.map(p => p.id)]; - } - } - - if (ids.length === 0) { - return { - items: [], - total: 0, - current, - pageSize, - }; - } - - where.id = In([...new Set(ids)]); - } - - where.on_delete = false; - - const products = await this.wpProductModel.find({ - relations: ['site'], - where, - skip: (current - 1) * pageSize, - take: pageSize, - }); - const total = await this.wpProductModel.count({ - where, - }); - if (products.length === 0) { - return { - items: [], - total, - current, - pageSize, - }; - } - - const variationQuery = this.wpProductModel - .createQueryBuilder('wp_product') - .leftJoin(Variation, 'variation', 'variation.productId = wp_product.id') - .leftJoin( - Product, - 'product', - 'wp_product.sku = product.sku' - ) - .leftJoin( - Product, - 'variation_product', - 'variation.sku = variation_product.sku' - ) - .select([ - 'wp_product.*', - 'variation.id as variation_id', - 'variation.siteId as variation_siteId', - 'variation.externalProductId as variation_externalProductId', - 'variation.externalVariationId as variation_externalVariationId', - 'variation.productId as variation_productId', - 'variation.sku as variation_sku', - 'variation.name as variation_name', - 'variation.regular_price as variation_regular_price', - 'variation.sale_price as variation_sale_price', - 'variation.on_sale as variation_on_sale', - 'product.name as product_name', // 关联查询返回 product.name - 'variation_product.name as variation_product_name', // 关联查询返回 variation 的产品 name - ]) - .where('wp_product.id IN (:...ids) AND wp_product.on_delete = false ', { - ids: products.map(product => product.id), - }); - - const rawResult = await variationQuery.getRawMany(); - - // 数据转换 - const items = rawResult.reduce((acc, row) => { - // 在累加器中查找当前产品 - let product = acc.find(p => p.id === row.id); - // 如果产品不存在,则创建新产品 - if (!product) { - // 从原始产品列表中查找,以获取 'site' 关联数据 - const originalProduct = products.find(p => p.id === row.id); - product = { - ...Object.keys(row) - .filter(key => !key.startsWith('variation_')) - .reduce((obj, key) => { - obj[key] = row[key]; - return obj; - }, {}), - variations: [], - // 附加 'site' 对象 - site: originalProduct.site, - }; - acc.push(product); - } - - if (row.variation_id) { - const variation: any = Object.keys(row) - .filter(key => key.startsWith('variation_')) - .reduce((obj, key) => { - obj[key.replace('variation_', '')] = row[key]; - return obj; - }, {}); - - product.variations.push(variation); - } - - return acc; - }, []); - - return { - items, - total, - current, - pageSize, - }; - } - - /** - * 检查 SKU 是否重复 - * @param sku SKU 编码 - * @param excludeSiteId 需要排除的站点 ID - * @param excludeProductId 需要排除的产品 ID - * @param excludeVariationId 需要排除的变体 ID - * @returns 是否重复 - */ - async isSkuDuplicate( - sku: string, - excludeSiteId?: number, - excludeProductId?: string, - excludeVariationId?: string - ): Promise { - if (!sku) return false; - const where: any = { sku }; - const varWhere: any = { sku }; - if (excludeVariationId) { - varWhere.siteId = Not(excludeSiteId); - varWhere.externalProductId = Not(excludeProductId); - varWhere.externalVariationId = Not(excludeVariationId); - } else if (excludeProductId) { - where.siteId = Not(excludeSiteId); - where.externalProductId = Not(excludeProductId); - } - - const productDuplicate = await this.wpProductModel.findOne({ - where, - }); - - if (productDuplicate) { - return true; - } - - const variationDuplicate = await this.variationModel.findOne({ - where: varWhere, - }); - - return !!variationDuplicate; - } - - async deleteById(id: number) { - const product = await this.wpProductModel.findOne({ where: { id } }); - if (!product) throw new Error('产品不存在'); - await this.delWpProduct(product.siteId, product.externalProductId); - return true; - } - - async delWpProduct(siteId: number, productId: string) { - const product = await this.wpProductModel.findOne({ - where: { siteId, externalProductId: productId }, - }); - if (!product) throw new Error('未找到该商品'); - - await this.variationModel.createQueryBuilder('variation') - .update() - .set({ on_delete: true }) - .where('variation.siteId = :siteId AND variation.externalProductId = :externalProductId', { siteId, externalProductId: productId }) - .execute(); - - const sums = await this.wpProductModel.createQueryBuilder('wp_product') - .update() - .set({ on_delete: true }) - .where('wp_product.siteId = :siteId AND wp_product.externalProductId = :externalProductId', { siteId, externalProductId: productId }) - .execute(); - - console.log(sums); - //await this.variationModel.delete({ siteId, externalProductId: productId }); - //await this.wpProductModel.delete({ siteId, externalProductId: productId }); - } - - - - async findProductsByName(name: string): Promise { - const nameFilter = name ? name.split(' ').filter(Boolean) : []; - const query = this.wpProductModel.createQueryBuilder('product'); - - // 保证 sku 不为空 - query.where('product.sku IS NOT NULL AND product.on_delete = false'); - - if (nameFilter.length > 0 || name) { - const params: Record = {}; - const conditions: string[] = []; - - // 英文名关键词全部匹配(AND) - if (nameFilter.length > 0) { - const nameConds = nameFilter.map((word, index) => { - const key = `name${index}`; - params[key] = `%${word}%`; - return `product.name LIKE :${key}`; - }); - conditions.push(`(${nameConds.join(' AND ')})`); - } - - // 中文名模糊匹配 - if (name) { - params['nameCn'] = `%${name}%`; - conditions.push(`product.nameCn LIKE :nameCn`); - } - - // 英文名关键词匹配 OR 中文名匹配 - query.andWhere(`(${conditions.join(' OR ')})`, params); - } - - query.take(50); - - return await query.getMany(); - } - - async syncToProduct(wpProductId: number) { - const wpProduct = await this.wpProductModel.findOne({ where: { id: wpProductId }, relations: ['site'] }); - if (!wpProduct) throw new Error('WpProduct not found'); - - const sku = wpProduct.sku; - if (!sku) throw new Error('WpProduct has no SKU'); - - // Try to find by main SKU - let product = await this.productModel.findOne({ where: { sku } }); - - // If not found, try to remove prefix if site has one - if (!product && wpProduct.site && wpProduct.site.skuPrefix && sku.startsWith(wpProduct.site.skuPrefix)) { - const skuWithoutPrefix = sku.slice(wpProduct.site.skuPrefix.length); - product = await this.productModel.findOne({ where: { sku: skuWithoutPrefix } }); - } - - // If still not found, try siteSkus - if (!product) { - const siteSku = await this.productSiteSkuModel.findOne({ where: { siteSku: sku }, relations: ['product'] }); - if (siteSku) { - product = siteSku.product; - } - } - - if (!product) { - throw new Error('Local Product not found for SKU: ' + sku); - } - - // Update fields - if (wpProduct.regular_price) product.price = Number(wpProduct.regular_price); - if (wpProduct.sale_price) product.promotionPrice = Number(wpProduct.sale_price); - - await this.productModel.save(product); - return true; - } - - /** - * 确保 SKU 存在于 ProductSiteSku 中,并根据 WpProduct 类型更新 Product 类型 - * @param sku - * @param siteId 站点ID,用于去除前缀 - * @param wpType WpProduct 类型 - */ - private async ensureSiteSku(sku: string, siteId?: number, wpType?: string) { - if (!sku) return; - // 查找本地产品 - let product = await this.productModel.findOne({ where: { sku } }); - - if (!product && siteId) { - // 如果找不到且有 siteId,尝试去除前缀再查找 - const site = await this.siteService.get(siteId, true); - if (site && site.skuPrefix && sku.startsWith(site.skuPrefix)) { - const skuWithoutPrefix = sku.slice(site.skuPrefix.length); - product = await this.productModel.findOne({ where: { sku: skuWithoutPrefix } }); - } - } - - if (product) { - // 更新产品类型 - if (wpType) { - // simple 对应 single, 其他对应 bundle - const targetType = wpType === 'simple' ? 'single' : 'bundle'; - if (product.type !== targetType) { - product.type = targetType; - await this.productModel.save(product); - } - } - - // 检查是否已存在 ProductSiteSku - const existingSiteSku = await this.productSiteSkuModel.findOne({ - where: { productId: product.id, siteSku: sku }, - }); - - if (!existingSiteSku) { - await this.productSiteSkuModel.save({ - productId: product.id, - siteSku: sku, - }); - } - } - } -} diff --git a/tsconfig.tsbuildinfo b/tsconfig.tsbuildinfo index 5924f59..ce5e83d 100644 --- a/tsconfig.tsbuildinfo +++ b/tsconfig.tsbuildinfo @@ -1 +1 @@ -{"root":["./src/configuration.ts","./src/interface.ts","./src/config/config.default.ts","./src/config/config.local.ts","./src/config/config.unittest.ts","./src/controller/api.controller.ts","./src/controller/area.controller.ts","./src/controller/customer.controller.ts","./src/controller/dict.controller.ts","./src/controller/locale.controller.ts","./src/controller/logistics.controller.ts","./src/controller/order.controller.ts","./src/controller/product.controller.ts","./src/controller/site.controller.ts","./src/controller/statistics.controller.ts","./src/controller/stock.controller.ts","./src/controller/subscription.controller.ts","./src/controller/template.controller.ts","./src/controller/user.controller.ts","./src/controller/webhook.controller.ts","./src/controller/wp_product.controller.ts","./src/db/datasource.ts","./src/db/migrations/1764238434984-product-dict-item-many-to-many.ts","./src/db/migrations/1764294088896-area.ts","./src/db/migrations/1764299629279-productstock.ts","./src/db/seeds/area.seeder.ts","./src/db/seeds/dict.seeder.ts","./src/db/seeds/template.seeder.ts","./src/decorator/user.decorator.ts","./src/dto/area.dto.ts","./src/dto/customer.dto.ts","./src/dto/dict.dto.ts","./src/dto/freightcom.dto.ts","./src/dto/logistics.dto.ts","./src/dto/order.dto.ts","./src/dto/product.dto.ts","./src/dto/reponse.dto.ts","./src/dto/site.dto.ts","./src/dto/statistics.dto.ts","./src/dto/stock.dto.ts","./src/dto/subscription.dto.ts","./src/dto/template.dto.ts","./src/dto/user.dto.ts","./src/dto/wp_product.dto.ts","./src/entity/area.entity.ts","./src/entity/auth_code.ts","./src/entity/customer.entity.ts","./src/entity/customer_tag.entity.ts","./src/entity/device_whitelist.ts","./src/entity/dict.entity.ts","./src/entity/dict_item.entity.ts","./src/entity/order.entity.ts","./src/entity/order_coupon.entity.ts","./src/entity/order_fee.entity.ts","./src/entity/order_item.entity.ts","./src/entity/order_item_original.entity.ts","./src/entity/order_items_original.entity.ts","./src/entity/order_note.entity.ts","./src/entity/order_refund.entity.ts","./src/entity/order_refund_item.entity.ts","./src/entity/order_sale.entity.ts","./src/entity/order_shipment.entity.ts","./src/entity/order_shipping.entity.ts","./src/entity/product.entity.ts","./src/entity/product_stock_component.entity.ts","./src/entity/purchase_order.entity.ts","./src/entity/purchase_order_item.entity.ts","./src/entity/service.entity.ts","./src/entity/shipment.entity.ts","./src/entity/shipment_item.entity.ts","./src/entity/shipping_address.entity.ts","./src/entity/site.entity.ts","./src/entity/stock.entity.ts","./src/entity/stock_point.entity.ts","./src/entity/stock_record.entity.ts","./src/entity/subscription.entity.ts","./src/entity/template.entity.ts","./src/entity/transfer.entity.ts","./src/entity/transfer_item.entity.ts","./src/entity/user.entity.ts","./src/entity/variation.entity.ts","./src/entity/wp_product.entity.ts","./src/enums/base.enum.ts","./src/filter/default.filter.ts","./src/filter/notfound.filter.ts","./src/job/sync_products.job.ts","./src/job/sync_shipment.job.ts","./src/middleware/auth.middleware.ts","./src/middleware/report.middleware.ts","./src/service/area.service.ts","./src/service/authcode.service.ts","./src/service/canadapost.service.ts","./src/service/customer.service.ts","./src/service/devicewhitelist.service.ts","./src/service/dict.service.ts","./src/service/freightcom.service.ts","./src/service/logistics.service.ts","./src/service/mail.service.ts","./src/service/order.service.ts","./src/service/product.service.ts","./src/service/site.service.ts","./src/service/statistics.service.ts","./src/service/stock.service.ts","./src/service/subscription.service.ts","./src/service/template.service.ts","./src/service/uni_express.service.ts","./src/service/user.service.ts","./src/service/wp.service.ts","./src/service/wp_product.service.ts","./src/utils/helper.util.ts","./src/utils/object-transform.util.ts","./src/utils/paginate.util.ts","./src/utils/paginated-response.util.ts","./src/utils/response-wrapper.util.ts","./src/utils/response.util.ts"],"version":"5.9.3"} \ No newline at end of file +{"root":["./src/configuration.ts","./src/interface.ts","./src/config/config.default.ts","./src/config/config.local.ts","./src/config/config.unittest.ts","./src/controller/api.controller.ts","./src/controller/area.controller.ts","./src/controller/customer.controller.ts","./src/controller/dict.controller.ts","./src/controller/locale.controller.ts","./src/controller/logistics.controller.ts","./src/controller/order.controller.ts","./src/controller/product.controller.ts","./src/controller/site.controller.ts","./src/controller/statistics.controller.ts","./src/controller/stock.controller.ts","./src/controller/subscription.controller.ts","./src/controller/template.controller.ts","./src/controller/user.controller.ts","./src/controller/webhook.controller.ts","./src/controller/wp_product.controller.ts","./src/db/datasource.ts","./src/db/migrations/1764238434984-product-dict-item-many-to-many.ts","./src/db/migrations/1764294088896-area.ts","./src/db/migrations/1764299629279-productstock.ts","./src/db/seeds/area.seeder.ts","./src/db/seeds/dict.seeder.ts","./src/db/seeds/template.seeder.ts","./src/decorator/user.decorator.ts","./src/dto/area.dto.ts","./src/dto/customer.dto.ts","./src/dto/dict.dto.ts","./src/dto/freightcom.dto.ts","./src/dto/logistics.dto.ts","./src/dto/order.dto.ts","./src/dto/product.dto.ts","./src/dto/reponse.dto.ts","./src/dto/site.dto.ts","./src/dto/statistics.dto.ts","./src/dto/stock.dto.ts","./src/dto/subscription.dto.ts","./src/dto/template.dto.ts","./src/dto/user.dto.ts","./src/dto/wp_product.dto.ts","./src/entity/area.entity.ts","./src/entity/auth_code.ts","./src/entity/customer.entity.ts","./src/entity/customer_tag.entity.ts","./src/entity/device_whitelist.ts","./src/entity/dict.entity.ts","./src/entity/dict_item.entity.ts","./src/entity/order.entity.ts","./src/entity/order_coupon.entity.ts","./src/entity/order_fee.entity.ts","./src/entity/order_item.entity.ts","./src/entity/order_item_original.entity.ts","./src/entity/order_items_original.entity.ts","./src/entity/order_note.entity.ts","./src/entity/order_refund.entity.ts","./src/entity/order_refund_item.entity.ts","./src/entity/order_sale.entity.ts","./src/entity/order_shipment.entity.ts","./src/entity/order_shipping.entity.ts","./src/entity/product.ts","./src/entity/product_stock_component.entity.ts","./src/entity/purchase_order.entity.ts","./src/entity/purchase_order_item.entity.ts","./src/entity/service.entity.ts","./src/entity/shipment.entity.ts","./src/entity/shipment_item.entity.ts","./src/entity/shipping_address.entity.ts","./src/entity/site.entity.ts","./src/entity/stock.entity.ts","./src/entity/stock_point.entity.ts","./src/entity/stock_record.entity.ts","./src/entity/subscription.entity.ts","./src/entity/template.entity.ts","./src/entity/transfer.entity.ts","./src/entity/transfer_item.entity.ts","./src/entity/user.entity.ts","./src/entity/variation.entity.ts","./src/entity/wp_product.ts","./src/enums/base.enum.ts","./src/filter/default.filter.ts","./src/filter/notfound.filter.ts","./src/job/sync_products.job.ts","./src/job/sync_shipment.job.ts","./src/middleware/auth.middleware.ts","./src/middleware/report.middleware.ts","./src/service/area.service.ts","./src/service/authcode.service.ts","./src/service/canadapost.service.ts","./src/service/customer.service.ts","./src/service/devicewhitelist.service.ts","./src/service/dict.service.ts","./src/service/freightcom.service.ts","./src/service/logistics.service.ts","./src/service/mail.service.ts","./src/service/order.service.ts","./src/service/product.service.ts","./src/service/site.service.ts","./src/service/statistics.service.ts","./src/service/stock.service.ts","./src/service/subscription.service.ts","./src/service/template.service.ts","./src/service/uni_express.service.ts","./src/service/user.service.ts","./src/service/wp.service.ts","./src/service/wp_product.service.ts","./src/utils/helper.util.ts","./src/utils/object-transform.util.ts","./src/utils/paginate.util.ts","./src/utils/paginated-response.util.ts","./src/utils/response-wrapper.util.ts","./src/utils/response.util.ts"],"version":"5.9.3"} \ No newline at end of file