From 0f5610e02e12e543bc83470f5df05db8dfd08925 Mon Sep 17 00:00:00 2001 From: tikkhun Date: Mon, 22 Dec 2025 17:30:47 +0800 Subject: [PATCH 1/8] =?UTF-8?q?refactor(api):=20=E7=BB=9F=E4=B8=80?= =?UTF-8?q?=E6=9F=A5=E8=AF=A2=E5=8F=82=E6=95=B0=E5=A4=84=E7=90=86=E9=80=BB?= =?UTF-8?q?=E8=BE=91=EF=BC=8C=E4=BD=BF=E7=94=A8where=E5=AF=B9=E8=B1=A1?= =?UTF-8?q?=E9=9B=86=E4=B8=AD=E7=AE=A1=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 将分散的查询参数如status、customer_id、ids等统一迁移到where对象中处理 简化DTO结构,移除冗余字段 适配器层统一从where对象获取查询条件 --- src/adapter/shopyy.adapter.ts | 6 +- src/adapter/woocommerce.adapter.ts | 80 ++++----------------------- src/controller/site-api.controller.ts | 28 ++++------ src/dto/site-api.dto.ts | 24 +------- 4 files changed, 28 insertions(+), 110 deletions(-) diff --git a/src/adapter/shopyy.adapter.ts b/src/adapter/shopyy.adapter.ts index b210e4a..2568db1 100644 --- a/src/adapter/shopyy.adapter.ts +++ b/src/adapter/shopyy.adapter.ts @@ -550,7 +550,7 @@ export class ShopyyAdapter implements ISiteAdapter { } private mapReviewSearchParams(params: UnifiedSearchParamsDTO): any { - const { search, page, per_page, status } = params; + const { search, page, per_page, where } = params; const shopyyParams: any = { page: page || 1, limit: per_page || 10, @@ -560,8 +560,8 @@ export class ShopyyAdapter implements ISiteAdapter { shopyyParams.search = search; } - if (status) { - shopyyParams.status = status; + if (where.status) { + shopyyParams.status = where.status; } // if (product_id) { diff --git a/src/adapter/woocommerce.adapter.ts b/src/adapter/woocommerce.adapter.ts index 64e1168..f716c1c 100644 --- a/src/adapter/woocommerce.adapter.ts +++ b/src/adapter/woocommerce.adapter.ts @@ -159,26 +159,10 @@ export class WooCommerceAdapter implements ISiteAdapter { const page = Number(params.page ?? 1); const per_page = Number( params.per_page ?? 20); const where = params.where && typeof params.where === 'object' ? params.where : {}; - let orderby: string | undefined = params.orderby; - let order: 'asc' | 'desc' | undefined = params.orderDir as any; - if (!orderby && params.order && typeof params.order === 'object') { - const entries = Object.entries(params.order as Record); - if (entries.length > 0) { - const [field, dir] = entries[0]; - let mappedField = field; - if (['created_at', 'date_created', 'date'].includes(field)) mappedField = 'date'; - else if (['name', 'title'].includes(field)) mappedField = 'title'; - else if (['id', 'ID'].includes(field)) mappedField = 'id'; - else if (['price', 'regular_price', 'sale_price'].includes(field)) mappedField = 'price'; - orderby = mappedField; - order = String(dir).toLowerCase() === 'desc' ? 'desc' : 'asc'; - } - } + const mapped: any = { ...(params.search ? { search: params.search } : {}), - ...(params.status ? { status: params.status } : {}), - ...(orderby ? { orderby } : {}), - ...(order ? { order } : {}), + ...(where.status ? { status: where.status } : {}), page, per_page, }; @@ -224,10 +208,6 @@ export class WooCommerceAdapter implements ISiteAdapter { if (where.virtual !== undefined) mapped.virtual = Boolean(where.virtual); if (where.downloadable !== undefined) mapped.downloadable = Boolean(where.downloadable); - if (params.ids) { - const idsArr = String(params.ids).split(',').map(v => v.trim()).filter(Boolean); - mapped.include = idsArr; - } return mapped; } @@ -237,30 +217,12 @@ export class WooCommerceAdapter implements ISiteAdapter { const per_page = Number( params.per_page ?? 20); // 解析排序参数 支持从 order 对象推导 const where = params.where && typeof params.where === 'object' ? params.where : {}; - let orderby: string | undefined = params.orderby; - let orderDir: 'asc' | 'desc' | undefined = params.orderDir as any; - if (!orderby && params.order && typeof params.order === 'object') { - const entries = Object.entries(params.order as Record); - if (entries.length > 0) { - const [field, dir] = entries[0]; - let mappedField = field; - if (['created_at', 'date_created', 'date'].includes(field)) mappedField = 'date'; - else if (['modified', 'date_modified'].includes(field)) mappedField = 'modified'; - else if (['id', 'ID'].includes(field)) mappedField = 'id'; - else if (['name', 'title'].includes(field)) mappedField = 'title'; - else if (['slug'].includes(field)) mappedField = 'slug'; - else if (['include'].includes(field)) mappedField = 'include'; - orderby = mappedField; - orderDir = String(dir).toLowerCase() === 'asc' ? 'asc' : 'desc'; - } - } else if (!orderDir && typeof params.order === 'string') { - orderDir = String(params.order).toLowerCase() === 'asc' ? 'asc' : 'desc'; - } - + + // if (params.orderBy && typeof params.orderBy === 'object') { + // } const mapped: any = { ...(params.search ? { search: params.search } : {}), - ...(orderby ? { orderby } : {}), - ...(orderDir ? { order: orderDir } : {}), + // ...(orderBy ? { orderBy } : {}), page, per_page, }; @@ -287,13 +249,13 @@ export class WooCommerceAdapter implements ISiteAdapter { // 集合过滤参数 if (where.exclude) mapped.exclude = toArray(where.exclude); if (where.include) mapped.include = toArray(where.include); - if (params.ids) mapped.include = String(params.ids).split(',').map(v => v.trim()).filter(Boolean); + if (where.ids) mapped.include = toArray(where.ids); if (toNumber(where.offset) !== undefined) mapped.offset = Number(where.offset); if (where.parent ?? where.parentId) mapped.parent = toArray(where.parent ?? where.parentId); if (where.parent_exclude ?? where.parentExclude) mapped.parent_exclude = toArray(where.parent_exclude ?? where.parentExclude); // 状态过滤 参数支持数组或逗号分隔字符串 - const statusSource = params.status ?? where.status; + const statusSource = where.status; if (statusSource !== undefined) { mapped.status = Array.isArray(statusSource) ? statusSource.map(s => String(s)) @@ -301,7 +263,7 @@ export class WooCommerceAdapter implements ISiteAdapter { } // 客户与产品过滤 - const customerVal = params.customer_id ?? where.customer ?? where.customer_id; + const customerVal = where.customer ?? where.customer_id; const productVal = where.product ?? where.product_id; const dpVal = where.dp; if (toNumber(customerVal) !== undefined) mapped.customer = Number(customerVal); @@ -321,28 +283,10 @@ export class WooCommerceAdapter implements ISiteAdapter { const page = Number(params.page ?? 1); const per_page = Number(params.per_page ?? 20); const where = params.where && typeof params.where === 'object' ? params.where : {}; - let orderby: string | undefined = params.orderby; - let orderDir: 'asc' | 'desc' | undefined = params.orderDir as any; - if (!orderby && params.order && typeof params.order === 'object') { - const entries = Object.entries(params.order as Record); - if (entries.length > 0) { - const [field, dir] = entries[0]; - let mappedField = field; - if (['id', 'ID'].includes(field)) mappedField = 'id'; - else if (['include'].includes(field)) mappedField = 'include'; - else if (['name', 'username'].includes(field)) mappedField = 'name'; - else if (['registered_date', 'date_created', 'registered', 'registeredDate'].includes(field)) mappedField = 'registered_date'; - orderby = mappedField; - orderDir = String(dir).toLowerCase() === 'asc' ? 'asc' : 'desc'; - } - } else if (!orderDir && typeof params.order === 'string') { - orderDir = String(params.order).toLowerCase() === 'asc' ? 'asc' : 'desc'; - } + const mapped: any = { ...(params.search ? { search: params.search } : {}), - ...(orderby ? { orderby } : {}), - ...(orderDir ? { order: orderDir } : {}), page, per_page, }; @@ -361,11 +305,11 @@ export class WooCommerceAdapter implements ISiteAdapter { if (where.exclude) mapped.exclude = toArray(where.exclude); if (where.include) mapped.include = toArray(where.include); - if (params.ids) mapped.include = String(params.ids).split(',').map(v => v.trim()).filter(Boolean); + if (where.ids) mapped.include = toArray(where.ids); if (toNumber(where.offset) !== undefined) mapped.offset = Number(where.offset); if (where.email) mapped.email = String(where.email); - const roleSource = where.role ?? params.status; + const roleSource = where.role; if (roleSource !== undefined) mapped.role = String(roleSource); return mapped; diff --git a/src/controller/site-api.controller.ts b/src/controller/site-api.controller.ts index 0518cba..660e589 100644 --- a/src/controller/site-api.controller.ts +++ b/src/controller/site-api.controller.ts @@ -270,8 +270,8 @@ export class SiteApiController { page += 1; } let items = all; - if (query.ids) { - const ids = new Set(String(query.ids).split(',').map(v => v.trim()).filter(Boolean)); + if (query.where?.ids) { + const ids = new Set(String(query.where.ids).split(',').map(v => v.trim()).filter(Boolean)); items = items.filter(i => ids.has(String(i.id))); } const header = ['id', 'name', 'type', 'status', 'sku', 'regular_price', 'sale_price', 'price', 'stock_status', 'stock_quantity', 'image_src']; @@ -600,10 +600,6 @@ export class SiteApiController { try { const adapter = await this.siteApiService.getAdapter(siteId); const where = { ...(query.where || {}) }; - if (query.customer_id) { - where.customer = query.customer_id; - where.customer_id = query.customer_id; - } const data = await adapter.getOrders({ ...query, where }); this.logger.info(`[Site API] 获取订单列表成功, siteId: ${siteId}, 共获取到 ${data.total} 个订单`); return successResponse(data); @@ -623,8 +619,8 @@ export class SiteApiController { this.logger.info(`[Site API] 获取客户订单列表开始, siteId: ${siteId}, customerId: ${customerId}, query: ${JSON.stringify(query)}`); try { const adapter = await this.siteApiService.getAdapter(siteId); - const where = { ...(query.where || {}), customer: customerId, customer_id: customerId }; - const data = await adapter.getOrders({ ...query, where, customer_id: customerId }); + const where = { ...(query.where || {}), customer: customerId }; + const data = await adapter.getOrders({ ...query, where }); this.logger.info(`[Site API] 获取客户订单列表成功, siteId: ${siteId}, customerId: ${customerId}, 共获取到 ${data.total} 个订单`); return successResponse(data); } catch (error) { @@ -652,8 +648,8 @@ export class SiteApiController { page += 1; } let items = all; - if (query.ids) { - const ids = new Set(String(query.ids).split(',').map(v => v.trim()).filter(Boolean)); + if (query.where?.ids) { + const ids = new Set(String(query.where.ids).split(',').map(v => v.trim()).filter(Boolean)); items = items.filter(i => ids.has(String(i.id))); } const header = ['id', 'number', 'status', 'currency', 'total', 'customer_id', 'customer_name', 'email', 'payment_method', 'phone', 'billing_full_address', 'shipping_full_address', 'date_created']; @@ -1005,8 +1001,8 @@ export class SiteApiController { page += 1; } let items = all; - if (query.ids) { - const ids = new Set(String(query.ids).split(',').map(v => v.trim()).filter(Boolean)); + if (query.where?.ids) { + const ids = new Set(String(query.where.ids).split(',').map(v => v.trim()).filter(Boolean)); items = items.filter(i => ids.has(String(i.id))); } const header = ['id', 'status', 'customer_id', 'billing_period', 'billing_interval', 'start_date', 'next_payment_date']; @@ -1055,8 +1051,8 @@ export class SiteApiController { page += 1; } let items = all; - if (query.ids) { - const ids = new Set(String(query.ids).split(',').map(v => v.trim()).filter(Boolean)); + if (query.where?.ids) { + const ids = new Set(String(query.where.ids).split(',').map(v => v.trim()).filter(Boolean)); items = items.filter(i => ids.has(String(i.id))); } const header = ['id', 'title', 'media_type', 'mime_type', 'source_url', 'date_created']; @@ -1220,10 +1216,6 @@ export class SiteApiController { page += 1; } let items = all; - if (query.ids) { - const ids = new Set(String(query.ids).split(',').map(v => v.trim()).filter(Boolean)); - items = items.filter(i => ids.has(String(i.id))); - } const header = ['id', 'email', 'first_name', 'last_name', 'fullname', 'username', 'phone', 'orders', 'total_spend', 'role', 'billing_full_address', 'shipping_full_address', 'date_created']; const formatAddress = (addr: any) => [ addr?.fullname, diff --git a/src/dto/site-api.dto.ts b/src/dto/site-api.dto.ts index fc9b52a..1ebdfa3 100644 --- a/src/dto/site-api.dto.ts +++ b/src/dto/site-api.dto.ts @@ -529,7 +529,7 @@ export class UploadMediaDTO { filename: string; } -export class UnifiedSearchParamsDTO { +export class UnifiedSearchParamsDTO> { // 统一查询参数DTO用于承载分页与筛选与排序参数 @ApiProperty({ description: '页码', example: 1, required: false }) page?: number; @@ -537,36 +537,18 @@ export class UnifiedSearchParamsDTO { @ApiProperty({ description: '每页数量', example: 20, required: false }) per_page?: number; - @ApiProperty({ description: '每页数量别名', example: 20, required: false }) - page_size?: number; - @ApiProperty({ description: '搜索关键词', required: false }) search?: string; - @ApiProperty({ description: '状态', required: false }) - status?: string; - - @ApiProperty({ description: '客户ID,用于筛选订单', required: false }) - customer_id?: number; - @ApiProperty({ description: '过滤条件对象', type: 'object', required: false }) - where?: Record; + where?: Where; @ApiProperty({ description: '排序对象,例如 { "sku": "desc" }', type: 'object', required: false, }) - order?: Record | string; - - @ApiProperty({ description: '排序字段(兼容旧入参)', required: false }) - orderby?: string; - - @ApiProperty({ description: '排序方式(兼容旧入参)', required: false }) - orderDir?: 'asc' | 'desc'; - - @ApiProperty({ description: '选中ID列表,逗号分隔', required: false }) - ids?: string; + orderBy?: Record | string; } export class UnifiedWebhookDTO { -- 2.40.1 From bc2ed4615e40338c2440d97059ea51533537c806 Mon Sep 17 00:00:00 2001 From: tikkhun Date: Tue, 23 Dec 2025 19:33:03 +0800 Subject: [PATCH 2/8] =?UTF-8?q?feat(customer):=20=E5=AE=9E=E7=8E=B0?= =?UTF-8?q?=E5=AE=A2=E6=88=B7=E6=95=B0=E6=8D=AE=E5=90=8C=E6=AD=A5=E5=8A=9F?= =?UTF-8?q?=E8=83=BD=E5=B9=B6=E5=A2=9E=E5=BC=BA=E5=AE=A2=E6=88=B7=E7=AE=A1?= =?UTF-8?q?=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 重构客户服务层,添加客户数据同步功能 扩展客户实体字段以支持完整客户信息存储 优化客户列表查询性能并添加统计功能 移除废弃的WpSite相关代码和配置 --- src/adapter/woocommerce.adapter.ts | 2 +- src/config/config.default.ts | 11 - src/config/config.local.ts | 48 ++-- src/configuration.ts | 3 - src/controller/customer.controller.ts | 36 ++- src/controller/site-api.controller.ts | 80 +++++-- src/controller/site.controller.ts | 4 +- src/controller/webhook.controller.ts | 7 +- src/dto/batch.dto.ts | 210 +++++++++++++++++ src/dto/customer.dto.ts | 24 ++ src/dto/reponse.dto.ts | 2 +- src/entity/customer.entity.ts | 47 +++- src/interface.ts | 9 - src/interface/site-adapter.interface.ts | 7 +- src/middleware/auth.middleware.ts | 7 + src/service/customer.service.ts | 291 +++++++++++++++++++++++- src/service/order.service.ts | 4 +- src/service/shopyy.service.ts | 35 ++- src/service/site.service.ts | 24 -- src/service/wp.service.ts | 116 +++++++++- src/service/wp_product.service.ts | 2 +- 21 files changed, 855 insertions(+), 114 deletions(-) create mode 100644 src/dto/batch.dto.ts diff --git a/src/adapter/woocommerce.adapter.ts b/src/adapter/woocommerce.adapter.ts index f716c1c..9b200cf 100644 --- a/src/adapter/woocommerce.adapter.ts +++ b/src/adapter/woocommerce.adapter.ts @@ -752,7 +752,6 @@ export class WooCommerceAdapter implements ISiteAdapter { raw: item, }; } - async getCustomers(params: UnifiedSearchParamsDTO): Promise> { const requestParams = this.mapCustomerSearchParams(params); const { items, total, totalPages, page, per_page } = await this.wpService.fetchResourcePaged( @@ -794,3 +793,4 @@ export class WooCommerceAdapter implements ISiteAdapter { return true; } } + diff --git a/src/config/config.default.ts b/src/config/config.default.ts index 3314dfe..e386aa6 100644 --- a/src/config/config.default.ts +++ b/src/config/config.default.ts @@ -116,17 +116,6 @@ export default { // secret: 'YOONE2024!@abc', // expiresIn: '7d', // }, - // wpSite: [ - // { - // id: '2', - // wpApiUrl: 'http://localhost:10004', - // consumerKey: 'ck_dc9e151e9048c8ed3e27f35ac79d2bf7d6840652', - // consumerSecret: 'cs_d05d625d7b0ac05c6d765671d8417f41d9477e38', - // name: 'Local', - // email: 'tom@yoonevape.com', - // emailPswd: '', - // }, - // ], swagger: { auth: { name: 'authorization', diff --git a/src/config/config.local.ts b/src/config/config.local.ts index a05235b..0c962ce 100644 --- a/src/config/config.local.ts +++ b/src/config/config.local.ts @@ -16,8 +16,10 @@ export default { dataSource: { default: { host: 'localhost', + port: "23306", username: 'root', password: '12345678', + database: 'inventory', }, }, }, @@ -25,7 +27,7 @@ export default { origin: '*', // 允许所有来源跨域请求 allowMethods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'], // 允许的 HTTP 方法 allowHeaders: ['Content-Type', 'Authorization'], // 允许的自定义请求头 - credentials: true, // 允许携带凭据(cookies等) + credentials: true, // 允许携带凭据(cookies等) }, jwt: { secret: 'YOONE2024!@abc', @@ -33,34 +35,38 @@ export default { }, wpSite: [ { - id: '-1', - siteName: 'Admin', - email: '2469687281@qq.com', - }, - { - id: '2', - wpApiUrl: 'http://t2-shop.local/', - consumerKey: 'ck_a369473a6451dbaec63d19cbfd74a074b2c5f742', - consumerSecret: 'cs_0946bbbeea1bfefff08a69e817ac62a48412df8c', - siteName: 'Local', - email: '2469687281@qq.com', - emailPswd: 'lulin91.', - }, - { - id: '3', - wpApiUrl: 'http://t1-shop.local/', - consumerKey: 'ck_a369473a6451dbaec63d19cbfd74a074b2c5f742', - consumerSecret: 'cs_0946bbbeea1bfefff08a69e817ac62a48412df8c', - siteName: 'Local-test-2', + id: '200', + wpApiUrl: "http://simple.local", + consumerKey: 'ck_11b446d0dfd221853830b782049cf9a17553f886', + consumerSecret: 'cs_2b06729269f659dcef675b8cdff542bf3c1da7e8', + name: 'LocalSimple', email: '2469687281@qq.com', emailPswd: 'lulin91.', }, // { // id: '2', + // wpApiUrl: 'http://t2-shop.local/', + // consumerKey: 'ck_a369473a6451dbaec63d19cbfd74a074b2c5f742', + // consumerSecret: 'cs_0946bbbeea1bfefff08a69e817ac62a48412df8c', + // name: 'Local', + // email: '2469687281@qq.com', + // emailPswd: 'lulin91.', + // }, + // { + // id: '3', + // wpApiUrl: 'http://t1-shop.local/', + // consumerKey: 'ck_a369473a6451dbaec63d19cbfd74a074b2c5f742', + // consumerSecret: 'cs_0946bbbeea1bfefff08a69e817ac62a48412df8c', + // name: 'Local-test-2', + // email: '2469687281@qq.com', + // emailPswd: 'lulin91.', + // }, + // { + // id: '2', // wpApiUrl: 'http://localhost:10004', // consumerKey: 'ck_dc9e151e9048c8ed3e27f35ac79d2bf7d6840652', // consumerSecret: 'cs_d05d625d7b0ac05c6d765671d8417f41d9477e38', - // siteName: 'Local', + // name: 'Local', // email: 'tom@yoonevape.com', // emailPswd: 'lulin91.', // }, diff --git a/src/configuration.ts b/src/configuration.ts index 3bd7813..b64dbd0 100644 --- a/src/configuration.ts +++ b/src/configuration.ts @@ -81,8 +81,5 @@ export class MainConfiguration { } } ); - - const sites = this.app.getConfig('wpSite') || []; - await this.siteService.syncFromConfig(sites); } } diff --git a/src/controller/customer.controller.ts b/src/controller/customer.controller.ts index 9225aa5..96c96ff 100644 --- a/src/controller/customer.controller.ts +++ b/src/controller/customer.controller.ts @@ -3,6 +3,7 @@ import { successResponse, errorResponse } from '../utils/response.util'; import { CustomerService } from '../service/customer.service'; import { QueryCustomerListDTO, CustomerTagDTO } from '../dto/customer.dto'; import { ApiOkResponse } from '@midwayjs/swagger'; +import { UnifiedSearchParamsDTO } from '../dto/site-api.dto'; @Controller('/customer') export class CustomerController { @@ -13,7 +14,18 @@ export class CustomerController { @Get('/getcustomerlist') async getCustomerList(@Query() query: QueryCustomerListDTO) { try { - const result = await this.customerService.getCustomerList(query as any); + const result = await this.customerService.getCustomerList(query) + return successResponse(result); + } catch (error) { + return errorResponse(error.message); + } + } + + @ApiOkResponse({ type: Object }) + @Get('/getcustomerstatisticlist') + async getCustomerStatisticList(@Query() query: QueryCustomerListDTO) { + try { + const result = await this.customerService.getCustomerStatisticList(query as any); return successResponse(result); } catch (error) { return errorResponse(error.message); @@ -63,4 +75,24 @@ export class CustomerController { return errorResponse(error.message); } } -} + + /** + * 同步客户数据 + * 从指定站点获取客户数据并保存到本地数据库 + * 业务逻辑已移到service层,controller只负责参数传递和响应 + */ + @ApiOkResponse({ type: Object }) + @Post('/sync') + async syncCustomers(@Body() body: { siteId: number; params?: UnifiedSearchParamsDTO }) { + try { + const { siteId, params = {} } = body; + + // 调用service层的同步方法,所有业务逻辑都在service中处理 + const syncResult = await this.customerService.syncCustomersFromSite(siteId, params); + + return successResponse(syncResult); + } catch (error) { + return errorResponse(error.message); + } + } +} \ No newline at end of file diff --git a/src/controller/site-api.controller.ts b/src/controller/site-api.controller.ts index 660e589..0cfa695 100644 --- a/src/controller/site-api.controller.ts +++ b/src/controller/site-api.controller.ts @@ -23,6 +23,7 @@ import { CancelShipOrderDTO, BatchShipOrdersDTO, } from '../dto/site-api.dto'; +import { BatchOperationDTO, BatchOperationResultDTO } from '../dto/batch.dto'; import { SiteApiService } from '../service/site-api.service'; import { errorResponse, successResponse } from '../utils/response.util'; import { ILogger } from '@midwayjs/core'; @@ -533,10 +534,10 @@ export class SiteApiController { } @Post('/:siteId/products/batch') - @ApiOkResponse({ type: Object }) + @ApiOkResponse({ type: BatchOperationResultDTO }) async batchProducts( @Param('siteId') siteId: number, - @Body() body: { create?: any[]; update?: any[]; delete?: Array } + @Body() body: BatchOperationDTO ) { this.logger.info(`[Site API] 批量处理产品开始, siteId: ${siteId}`); try { @@ -549,14 +550,18 @@ export class SiteApiController { const created: any[] = []; const updated: any[] = []; const deleted: Array = []; - const failed: any[] = []; + const errors: Array<{identifier: string, error: string}> = []; + if (body.create?.length) { for (const item of body.create) { try { const data = await adapter.createProduct(item); created.push(data); } catch (e) { - failed.push({ action: 'create', item, error: (e as any).message }); + errors.push({ + identifier: String(item.id || item.sku || 'unknown'), + error: (e as any).message + }); } } } @@ -567,7 +572,10 @@ export class SiteApiController { const data = await adapter.updateProduct(id, item); updated.push(data); } catch (e) { - failed.push({ action: 'update', item, error: (e as any).message }); + errors.push({ + identifier: String(item.id || 'unknown'), + error: (e as any).message + }); } } } @@ -576,14 +584,28 @@ export class SiteApiController { try { const ok = await adapter.deleteProduct(id); if (ok) deleted.push(id); - else failed.push({ action: 'delete', id, error: 'delete failed' }); + else errors.push({ + identifier: String(id), + error: 'delete failed' + }); } catch (e) { - failed.push({ action: 'delete', id, error: (e as any).message }); + errors.push({ + identifier: String(id), + error: (e as any).message + }); } } } + this.logger.info(`[Site API] 批量处理产品完成, siteId: ${siteId}`); - return successResponse({ created, updated, deleted, failed }); + return successResponse({ + total: (body.create?.length || 0) + (body.update?.length || 0) + (body.delete?.length || 0), + processed: created.length + updated.length + deleted.length, + created: created.length, + updated: updated.length, + deleted: deleted.length, + errors: errors + }); } catch (error) { this.logger.error(`[Site API] 批量处理产品失败, siteId: ${siteId}, 错误信息: ${error.message}`); return errorResponse(error.message); @@ -789,10 +811,10 @@ export class SiteApiController { } @Post('/:siteId/orders/batch') - @ApiOkResponse({ type: Object }) + @ApiOkResponse({ type: BatchOperationResultDTO }) async batchOrders( @Param('siteId') siteId: number, - @Body() body: { create?: any[]; update?: any[]; delete?: Array } + @Body() body: BatchOperationDTO ) { this.logger.info(`[Site API] 批量处理订单开始, siteId: ${siteId}`); try { @@ -800,14 +822,18 @@ export class SiteApiController { const created: any[] = []; const updated: any[] = []; const deleted: Array = []; - const failed: any[] = []; + const errors: Array<{identifier: string, error: string}> = []; + if (body.create?.length) { for (const item of body.create) { try { const data = await adapter.createOrder(item); created.push(data); } catch (e) { - failed.push({ action: 'create', item, error: (e as any).message }); + errors.push({ + identifier: String(item.id || item.order_number || 'unknown'), + error: (e as any).message + }); } } } @@ -817,9 +843,15 @@ export class SiteApiController { const id = item.id; const ok = await adapter.updateOrder(id, item); if (ok) updated.push(item); - else failed.push({ action: 'update', item, error: 'update failed' }); + else errors.push({ + identifier: String(item.id || 'unknown'), + error: 'update failed' + }); } catch (e) { - failed.push({ action: 'update', item, error: (e as any).message }); + errors.push({ + identifier: String(item.id || 'unknown'), + error: (e as any).message + }); } } } @@ -828,14 +860,28 @@ export class SiteApiController { try { const ok = await adapter.deleteOrder(id); if (ok) deleted.push(id); - else failed.push({ action: 'delete', id, error: 'delete failed' }); + else errors.push({ + identifier: String(id), + error: 'delete failed' + }); } catch (e) { - failed.push({ action: 'delete', id, error: (e as any).message }); + errors.push({ + identifier: String(id), + error: (e as any).message + }); } } } + this.logger.info(`[Site API] 批量处理订单完成, siteId: ${siteId}`); - return successResponse({ created, updated, deleted, failed }); + return successResponse({ + total: (body.create?.length || 0) + (body.update?.length || 0) + (body.delete?.length || 0), + processed: created.length + updated.length + deleted.length, + created: created.length, + updated: updated.length, + deleted: deleted.length, + errors: errors + }); } catch (error) { this.logger.error(`[Site API] 批量处理订单失败, siteId: ${siteId}, 错误信息: ${error.message}`); return errorResponse(error.message); diff --git a/src/controller/site.controller.ts b/src/controller/site.controller.ts index 02cf312..3427a33 100644 --- a/src/controller/site.controller.ts +++ b/src/controller/site.controller.ts @@ -1,6 +1,6 @@ import { Body, Controller, Get, Inject, Param, Put, Post, Query } from '@midwayjs/core'; import { ApiOkResponse } from '@midwayjs/swagger'; -import { WpSitesResponse } from '../dto/reponse.dto'; +import { SitesResponse } from '../dto/reponse.dto'; import { errorResponse, successResponse } from '../utils/response.util'; import { SiteService } from '../service/site.service'; import { CreateSiteDTO, DisableSiteDTO, QuerySiteDTO, UpdateSiteDTO } from '../dto/site.dto'; @@ -10,7 +10,7 @@ export class SiteController { @Inject() siteService: SiteService; - @ApiOkResponse({ description: '关联网站', type: WpSitesResponse }) + @ApiOkResponse({ description: '关联网站', type: SitesResponse }) @Get('/all') async all() { try { diff --git a/src/controller/webhook.controller.ts b/src/controller/webhook.controller.ts index c4398b0..c4ad821 100644 --- a/src/controller/webhook.controller.ts +++ b/src/controller/webhook.controller.ts @@ -1,4 +1,4 @@ -import { HttpStatus, Inject } from '@midwayjs/core'; +import { HttpStatus, ILogger, Inject, Logger } from '@midwayjs/core'; import { Controller, Post, @@ -25,6 +25,9 @@ export class WebhookController { @Inject() ctx: Context; + @Logger() + logger: ILogger; + @Inject() private readonly siteService: SiteService; @@ -48,7 +51,7 @@ export class WebhookController { // 从数据库获取站点配置 const site = await this.siteService.get(siteId, true); - if (!site || !source.includes(site.apiUrl)) { + if (!site || !source?.includes(site.apiUrl)) { console.log('domain not match'); return { code: HttpStatus.BAD_REQUEST, diff --git a/src/dto/batch.dto.ts b/src/dto/batch.dto.ts new file mode 100644 index 0000000..67b11cd --- /dev/null +++ b/src/dto/batch.dto.ts @@ -0,0 +1,210 @@ +import { ApiProperty } from '@midwayjs/swagger'; +import { Rule, RuleType } from '@midwayjs/validate'; + +/** + * 批量操作错误项 + */ +export interface BatchErrorItem { + // 错误项标识(可以是ID、邮箱等) + identifier: string; + // 错误信息 + error: string; +} + +/** + * 批量操作结果基础接口 + */ +export interface BatchOperationResult { + // 总处理数量 + total: number; + // 成功处理数量 + processed: number; + // 创建数量 + created?: number; + // 更新数量 + updated?: number; + // 删除数量 + deleted?: number; + // 跳过的数量(如数据已存在或无需处理) + skipped?: number; + // 错误列表 + errors: BatchErrorItem[]; +} + +/** + * 同步操作结果接口 + */ +export interface SyncOperationResult extends BatchOperationResult { + // 同步成功数量 + synced: number; +} + +/** + * 批量操作错误项DTO + */ +export class BatchErrorItemDTO { + @ApiProperty({ description: '错误项标识(如ID、邮箱等)', type: String }) + @Rule(RuleType.string().required()) + identifier: string; + + @ApiProperty({ description: '错误信息', type: String }) + @Rule(RuleType.string().required()) + error: string; +} + +/** + * 批量操作结果基础DTO + */ +export class BatchOperationResultDTO { + @ApiProperty({ description: '总处理数量', type: Number }) + total: number; + + @ApiProperty({ description: '成功处理数量', type: Number }) + processed: number; + + @ApiProperty({ description: '创建数量', type: Number, required: false }) + created?: number; + + @ApiProperty({ description: '更新数量', type: Number, required: false }) + updated?: number; + + @ApiProperty({ description: '删除数量', type: Number, required: false }) + deleted?: number; + + @ApiProperty({ description: '跳过的数量', type: Number, required: false }) + skipped?: number; + + @ApiProperty({ description: '错误列表', type: [BatchErrorItemDTO] }) + errors: BatchErrorItemDTO[]; +} + +/** + * 同步操作结果DTO + */ +export class SyncOperationResultDTO extends BatchOperationResultDTO { + @ApiProperty({ description: '同步成功数量', type: Number }) + synced: number; +} + +/** + * 批量创建DTO + */ +export class BatchCreateDTO { + @ApiProperty({ description: '要创建的数据列表', type: Array }) + @Rule(RuleType.array().required()) + items: T[]; +} + +/** + * 批量更新DTO + */ +export class BatchUpdateDTO { + @ApiProperty({ description: '要更新的数据列表', type: Array }) + @Rule(RuleType.array().required()) + items: T[]; +} + +/** + * 批量删除DTO + */ +export class BatchDeleteDTO { + @ApiProperty({ description: '要删除的ID列表', type: [String, Number] }) + @Rule(RuleType.array().items(RuleType.alternatives().try(RuleType.string(), RuleType.number())).required()) + ids: Array; +} + +/** + * 批量操作请求DTO(包含增删改) + */ +export class BatchOperationDTO { + @ApiProperty({ description: '要创建的数据列表', type: Array, required: false }) + @Rule(RuleType.array().optional()) + create?: T[]; + + @ApiProperty({ description: '要更新的数据列表', type: Array, required: false }) + @Rule(RuleType.array().optional()) + update?: T[]; + + @ApiProperty({ description: '要删除的ID列表', type: [String, Number], required: false }) + @Rule(RuleType.array().items(RuleType.alternatives().try(RuleType.string(), RuleType.number())).optional()) + delete?: Array; +} + +/** + * 分页批量操作DTO + */ +export class PaginatedBatchOperationDTO { + @ApiProperty({ description: '页码', type: Number, required: false, default: 1 }) + @Rule(RuleType.number().integer().min(1).optional()) + page?: number = 1; + + @ApiProperty({ description: '每页数量', type: Number, required: false, default: 100 }) + @Rule(RuleType.number().integer().min(1).max(1000).optional()) + pageSize?: number = 100; + + @ApiProperty({ description: '要创建的数据列表', type: Array, required: false }) + @Rule(RuleType.array().optional()) + create?: T[]; + + @ApiProperty({ description: '要更新的数据列表', type: Array, required: false }) + @Rule(RuleType.array().optional()) + update?: T[]; + + @ApiProperty({ description: '要删除的ID列表', type: [String, Number], required: false }) + @Rule(RuleType.array().items(RuleType.alternatives().try(RuleType.string(), RuleType.number())).optional()) + delete?: Array; +} + +/** + * 同步参数DTO + */ +export class SyncParamsDTO { + @ApiProperty({ description: '页码', type: Number, required: false, default: 1 }) + @Rule(RuleType.number().integer().min(1).optional()) + page?: number = 1; + + @ApiProperty({ description: '每页数量', type: Number, required: false, default: 100 }) + @Rule(RuleType.number().integer().min(1).max(1000).optional()) + pageSize?: number = 100; + + @ApiProperty({ description: '开始时间', type: String, required: false }) + @Rule(RuleType.string().optional()) + startDate?: string; + + @ApiProperty({ description: '结束时间', type: String, required: false }) + @Rule(RuleType.string().optional()) + endDate?: string; + + @ApiProperty({ description: '强制同步(忽略缓存)', type: Boolean, required: false, default: false }) + @Rule(RuleType.boolean().optional()) + force?: boolean = false; +} + +/** + * 批量查询DTO + */ +export class BatchQueryDTO { + @ApiProperty({ description: 'ID列表', type: [String, Number] }) + @Rule(RuleType.array().items(RuleType.alternatives().try(RuleType.string(), RuleType.number())).required()) + ids: Array; + + @ApiProperty({ description: '包含关联数据', type: Boolean, required: false, default: false }) + @Rule(RuleType.boolean().optional()) + includeRelations?: boolean = false; +} + +/** + * 批量操作结果类(泛型支持) + */ +export class BatchOperationResultDTOGeneric extends BatchOperationResultDTO { + @ApiProperty({ description: '操作成功的数据列表', type: Array }) + data?: T[]; +} + +/** + * 同步操作结果类(泛型支持) + */ +export class SyncOperationResultDTOGeneric extends SyncOperationResultDTO { + @ApiProperty({ description: '同步成功的数据列表', type: Array }) + data?: T[]; +} \ No newline at end of file diff --git a/src/dto/customer.dto.ts b/src/dto/customer.dto.ts index 9fa62cd..8f56b5a 100644 --- a/src/dto/customer.dto.ts +++ b/src/dto/customer.dto.ts @@ -36,3 +36,27 @@ export class CustomerTagDTO { @ApiProperty() tag: string; } + +export class CustomerDto { + @ApiProperty() + id: number; + + @ApiProperty() + site_id: number; + + @ApiProperty() + email: string; + + @ApiProperty() + avatar: string; + + @ApiProperty() + tags: string[]; + + @ApiProperty() + rate: number; + + @ApiProperty() + state: string; + +} \ No newline at end of file diff --git a/src/dto/reponse.dto.ts b/src/dto/reponse.dto.ts index 6180703..0db709f 100644 --- a/src/dto/reponse.dto.ts +++ b/src/dto/reponse.dto.ts @@ -25,7 +25,7 @@ import { Dict } from '../entity/dict.entity'; export class BooleanRes extends SuccessWrapper(Boolean) {} //网站配置返回数据 -export class WpSitesResponse extends SuccessArrayWrapper(SiteConfig) {} +export class SitesResponse extends SuccessArrayWrapper(SiteConfig) {} //产品分页数据 export class ProductPaginatedResponse extends PaginatedWrapper(Product) {} //产品分页返回数据 diff --git a/src/entity/customer.entity.ts b/src/entity/customer.entity.ts index 0dcbd18..ae4fa04 100644 --- a/src/entity/customer.entity.ts +++ b/src/entity/customer.entity.ts @@ -1,13 +1,58 @@ -import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm'; +import { Column, Entity, PrimaryGeneratedColumn, CreateDateColumn, UpdateDateColumn } from 'typeorm'; @Entity('customer') export class Customer { @PrimaryGeneratedColumn() id: number; + @Column({ nullable: true }) + site_id: number; + + @Column({ nullable: true }) + origin_id: string; + @Column({ unique: true }) email: string; + @Column({ nullable: true }) + first_name: string; + + @Column({ nullable: true }) + last_name: string; + + @Column({ nullable: true }) + fullname: string; + + @Column({ nullable: true }) + username: string; + + @Column({ nullable: true }) + phone: string; + + @Column({ nullable: true }) + avatar: string; + + @Column({ type: 'json', nullable: true }) + billing: any; + + @Column({ type: 'json', nullable: true }) + shipping: any; + + @Column({ type: 'json', nullable: true }) + raw: any; + @Column({ default: 0}) rate: number; + + @CreateDateColumn() + created_at: Date; + + @UpdateDateColumn() + updated_at: Date; + + @Column({ nullable: true }) + site_created_at: Date; + + @Column({ nullable: true }) + site_updated_at: Date; } \ No newline at end of file diff --git a/src/interface.ts b/src/interface.ts index e000403..a133f62 100644 --- a/src/interface.ts +++ b/src/interface.ts @@ -5,15 +5,6 @@ export interface IUserOptions { uid: number; } -export interface WpSite { - id: string; - wpApiUrl: string; - consumerKey: string; - consumerSecret: string; - name: string; - email: string; - emailPswd: string; -} export interface PaginationParams { current?: number; // 当前页码 diff --git a/src/interface/site-adapter.interface.ts b/src/interface/site-adapter.interface.ts index 8872e3f..0ae0179 100644 --- a/src/interface/site-adapter.interface.ts +++ b/src/interface/site-adapter.interface.ts @@ -14,6 +14,7 @@ import { CreateWebhookDTO, UpdateWebhookDTO, } from '../dto/site-api.dto'; +import { BatchOperationDTO, BatchOperationResultDTO } from '../dto/batch.dto'; export interface ISiteAdapter { /** @@ -101,13 +102,13 @@ export interface ISiteAdapter { */ deleteProduct(id: string | number): Promise; - batchProcessProducts?(data: { create?: any[]; update?: any[]; delete?: Array }): Promise; + batchProcessProducts?(data: BatchOperationDTO): Promise; createOrder(data: Partial): Promise; updateOrder(id: string | number, data: Partial): Promise; deleteOrder(id: string | number): Promise; - batchProcessOrders?(data: { create?: any[]; update?: any[]; delete?: Array }): Promise; + batchProcessOrders?(data: BatchOperationDTO): Promise; getCustomers(params: UnifiedSearchParamsDTO): Promise>; getCustomer(id: string | number): Promise; @@ -115,7 +116,7 @@ export interface ISiteAdapter { updateCustomer(id: string | number, data: Partial): Promise; deleteCustomer(id: string | number): Promise; - batchProcessCustomers?(data: { create?: any[]; update?: any[]; delete?: Array }): Promise; + batchProcessCustomers?(data: BatchOperationDTO): Promise; /** * 获取webhooks列表 diff --git a/src/middleware/auth.middleware.ts b/src/middleware/auth.middleware.ts index 12675e0..69a1495 100644 --- a/src/middleware/auth.middleware.ts +++ b/src/middleware/auth.middleware.ts @@ -23,6 +23,13 @@ export class AuthMiddleware implements IMiddleware { '/webhook/woocommerce', '/logistics/getTrackingNumber', '/logistics/getListByTrackingId', + '/product/categories/all', + '/product/category/1/attributes', + '/product/category/2/attributes', + '/product/category/3/attributes', + '/product/category/4/attributes', + '/product/list', + '/dict/items', ]; match(ctx: Context) { diff --git a/src/service/customer.service.ts b/src/service/customer.service.ts index b039ecb..76bb4fb 100644 --- a/src/service/customer.service.ts +++ b/src/service/customer.service.ts @@ -1,9 +1,12 @@ -import { Provide } from '@midwayjs/core'; +import { Provide, Inject } from '@midwayjs/core'; import { InjectEntityModel } from '@midwayjs/typeorm'; import { Order } from '../entity/order.entity'; 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 { SyncOperationResult, BatchErrorItem } from '../dto/batch.dto'; @Provide() export class CustomerService { @@ -16,7 +19,183 @@ export class CustomerService { @InjectEntityModel(Customer) customerModel: Repository; - async getCustomerList(param: Record) { + @Inject() + siteApiService: SiteApiService; + + /** + * 根据邮箱查找客户 + */ + async findCustomerByEmail(email: string): Promise { + return await this.customerModel.findOne({ where: { email } }); + } + + /** + * 将站点客户数据映射为本地客户实体数据 + * 处理字段映射和数据转换,确保所有字段正确同步 + */ + private mapSiteCustomerToCustomer(siteCustomer: UnifiedCustomerDTO, siteId: number): Partial { + return { + site_id: siteId, // 使用站点ID而不是客户ID + origin_id: "" + siteCustomer.id, + email: siteCustomer.email, + first_name: siteCustomer.first_name, + last_name: siteCustomer.last_name, + fullname: siteCustomer.fullname || `${siteCustomer.first_name || ''} ${siteCustomer.last_name || ''}`.trim(), + username: siteCustomer.username || '', + phone: siteCustomer.phone || '', + avatar: siteCustomer.avatar, + billing: siteCustomer.billing, + shipping: siteCustomer.shipping, + raw: siteCustomer.raw || siteCustomer, + site_created_at: this.parseDate(siteCustomer.date_created), + site_updated_at: this.parseDate(siteCustomer.date_modified) + }; + } + + + /** + * 解析日期字符串或时间戳 + */ + private parseDate(dateValue: any): Date | null { + if (!dateValue) return null; + + if (dateValue instanceof Date) { + return dateValue; + } + + if (typeof dateValue === 'number') { + // 处理Unix时间戳(秒或毫秒) + return new Date(dateValue > 9999999999 ? dateValue : dateValue * 1000); + } + + if (typeof dateValue === 'string') { + const date = new Date(dateValue); + return isNaN(date.getTime()) ? null : date; + } + + return null; + } + + /** + * 创建新客户 + */ + async createCustomer(customerData: Partial): Promise { + const customer = this.customerModel.create(customerData); + return await this.customerModel.save(customer); + } + + /** + * 更新客户信息 + */ + async updateCustomer(id: number, customerData: Partial): Promise { + await this.customerModel.update(id, customerData); + return await this.customerModel.findOne({ where: { id } }); + } + + /** + * 创建或更新客户(upsert) + * 如果客户存在则更新,不存在则创建 + */ + async upsertCustomer( + customerData: Partial, + ): Promise<{ customer: Customer; isCreated: boolean }> { + if(!customerData.email) throw new Error("客户邮箱不能为空"); + // 首先尝试根据邮箱查找现有客户 + const existingCustomer = await this.findCustomerByEmail(customerData.email); + + if (existingCustomer) { + // 如果客户存在,更新客户信息 + const updatedCustomer = await this.updateCustomer(existingCustomer.id, customerData); + return { customer: updatedCustomer, isCreated: false }; + } else { + // 如果客户不存在,创建新客户 + const newCustomer = await this.createCustomer(customerData); + return { customer: newCustomer, isCreated: true }; + } + } + + /** + * 批量创建或更新客户 + * 使用事务确保数据一致性 + */ + async upsertManyCustomers( + customersData: Array> + ): Promise<{ + customers: Customer[]; + created: number; + updated: number; + processed: number; + errors: BatchErrorItem[]; + }> { + const results = { + customers: [], + created: 0, + updated: 0, + processed: 0, + errors: [] + }; + + // 批量处理每个客户 + for (const customerData of customersData) { + try { + const result = await this.upsertCustomer(customerData); + results.customers.push(result.customer); + + if (result.isCreated) { + results.created++; + } else { + results.updated++; + } + results.processed++; + } catch (error) { + // 记录错误但不中断整个批量操作 + results.errors.push({ + identifier: customerData.email || String(customerData.id) || 'unknown', + error: error.message + }); + } + } + + return results; + } + + /** + * 从站点同步客户数据 + * 第一步:调用adapter获取站点客户数据 + * 第二步:通过upsertManyCustomers保存这些客户 + */ + async syncCustomersFromSite( + siteId: number, + params?: UnifiedSearchParamsDTO + ): Promise { + try { + // 第一步:获取适配器并从站点获取客户数据 + const adapter = await this.siteApiService.getAdapter(siteId); + const siteCustomersResult = await adapter.getCustomers(params || {}); + + // 第二步:将站点客户数据转换为客户实体数据 + const customersData = siteCustomersResult.items.map(siteCustomer => { + return this.mapSiteCustomerToCustomer(siteCustomer, siteId); + }); + + // 第三步:批量upsert客户数据 + const upsertResult = await this.upsertManyCustomers(customersData); + return { + total: siteCustomersResult.total, + processed: upsertResult.customers.length, + synced: upsertResult.customers.length, + updated: upsertResult.updated, + created: upsertResult.created, + errors: upsertResult.errors + }; + + } catch (error) { + // 如果获取适配器或站点数据失败,抛出错误 + throw new Error(`同步客户数据失败: ${error.message}`); + } + } + + async getCustomerStatisticList(param: Record) { const { current = 1, pageSize = 10, @@ -148,6 +327,112 @@ export class CustomerService { }; } + /** + * 获取纯粹的客户列表(不包含订单统计信息) + * 支持基本的分页、搜索和排序功能 + * 使用TypeORM查询构建器实现 + */ + async getCustomerList(param: Record): Promise{ + const { + current = 1, + pageSize = 10, + email, + firstName, + lastName, + phone, + state, + rate, + sorterKey, + sorterValue, + } = param; + + // 创建查询构建器 + const queryBuilder = this.customerModel + .createQueryBuilder('c') + .leftJoinAndSelect( + 'customer_tag', + 'ct', + 'ct.email = c.email' + ) + .select([ + 'c.id', + 'c.email', + 'c.first_name', + 'c.last_name', + 'c.fullname', + 'c.username', + 'c.phone', + 'c.avatar', + 'c.billing', + 'c.shipping', + 'c.rate', + 'c.site_id', + 'c.created_at', + 'c.updated_at', + 'c.site_created_at', + 'c.site_updated_at', + 'GROUP_CONCAT(ct.tag) as tags' + ]) + .groupBy('c.id'); + + // 邮箱搜索 + if (email) { + queryBuilder.andWhere('c.email LIKE :email', { email: `%${email}%` }); + } + + // 姓名搜索 + if (firstName) { + queryBuilder.andWhere('c.first_name LIKE :firstName', { firstName: `%${firstName}%` }); + } + + if (lastName) { + queryBuilder.andWhere('c.last_name LIKE :lastName', { lastName: `%${lastName}%` }); + } + + // 电话搜索 + if (phone) { + queryBuilder.andWhere('c.phone LIKE :phone', { phone: `%${phone}%` }); + } + + // 省份搜索 + if (state) { + queryBuilder.andWhere("JSON_UNQUOTE(JSON_EXTRACT(c.billing, '$.state')) = :state", { state }); + } + + // 评分过滤 + if (rate !== undefined && rate !== null) { + queryBuilder.andWhere('c.rate = :rate', { rate: Number(rate) }); + } + + // 排序处理 + if (sorterKey) { + const order = sorterValue === 'descend' ? 'DESC' : 'ASC'; + queryBuilder.orderBy(`c.${sorterKey}`, order); + } else { + queryBuilder.orderBy('c.created_at', 'DESC'); + } + + // 分页 + queryBuilder.skip((current - 1) * pageSize).take(pageSize); + + // 执行查询 + const [items, total] = await queryBuilder.getManyAndCount(); + + // 处理tags字段,将逗号分隔的字符串转换为数组 + const processedItems = items.map(item => { + const plainItem = JSON.parse(JSON.stringify(item)); + plainItem.tags = plainItem.tags ? plainItem.tags.split(',').filter(tag => tag) : []; + return plainItem; + }); + + return { + items: processedItems, + total, + current, + pageSize, + }; + } + async addTag(email: string, tag: string) { const isExist = await this.customerTagModel.findOneBy({ email, tag }); @@ -172,4 +457,4 @@ export class CustomerService { async setRate(params: { id: number; rate: number }) { return await this.customerModel.update(params.id, { rate: params.rate }); } -} +} \ No newline at end of file diff --git a/src/service/order.service.ts b/src/service/order.service.ts index 76e630a..4686fe2 100644 --- a/src/service/order.service.ts +++ b/src/service/order.service.ts @@ -1,6 +1,5 @@ import { Inject, Provide } from '@midwayjs/core'; import { WPService } from './wp.service'; -import { WpSite } from '../interface'; import { Order } from '../entity/order.entity'; import { In, Like, Repository } from 'typeorm'; import { InjectEntityModel, TypeORMDataSourceManager } from '@midwayjs/typeorm'; @@ -1444,8 +1443,7 @@ export class OrderService { async cancelOrder(id: number) { const order = await this.orderModel.findOne({ where: { id } }); if (!order) throw new Error(`订单 ${id}不存在`); - const s: any = await this.siteService.get(Number(order.siteId), true); - const site = { id: String(s.id), wpApiUrl: s.apiUrl, consumerKey: s.consumerKey, consumerSecret: s.consumerSecret, name: s.name, email: '', emailPswd: '' } as WpSite; + 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, diff --git a/src/service/shopyy.service.ts b/src/service/shopyy.service.ts index d6eaa4b..900ba23 100644 --- a/src/service/shopyy.service.ts +++ b/src/service/shopyy.service.ts @@ -6,6 +6,7 @@ import { SiteService } from './site.service'; import { Site } from '../entity/site.entity'; import { UnifiedReviewDTO } from '../dto/site-api.dto'; import { ShopyyReview } from '../dto/shopyy.dto'; +import { BatchOperationDTO, BatchOperationResultDTO } from '../dto/batch.dto'; /** * ShopYY平台服务实现 @@ -533,10 +534,40 @@ export class ShopyyService { * @param data 批量操作数据 * @returns 处理结果 */ - async batchProcessProducts(site: any, data: { create?: any[]; update?: any[]; delete?: any[] }): Promise { + async batchProcessProducts(site: any, data: BatchOperationDTO): Promise { // ShopYY API: POST /products/batch const response = await this.request(site, 'products/batch', 'POST', data); - return response.data; + const result = response.data; + + // 转换 ShopYY 批量操作结果为统一格式 + const errors: Array<{identifier: string, error: string}> = []; + + // 假设 ShopYY 返回格式与 WooCommerce 类似: { create: [...], update: [...], delete: [...] } + // 错误信息可能在每个项目的 error 字段中 + const checkForErrors = (items: any[]) => { + items.forEach(item => { + if (item.error) { + errors.push({ + identifier: String(item.id || item.sku || 'unknown'), + error: typeof item.error === 'string' ? item.error : JSON.stringify(item.error) + }); + } + }); + }; + + // 检查每个操作类型的结果中的错误 + if (result.create) checkForErrors(result.create); + if (result.update) checkForErrors(result.update); + if (result.delete) checkForErrors(result.delete); + + return { + total: (data.create?.length || 0) + (data.update?.length || 0) + (data.delete?.length || 0), + processed: (result.create?.length || 0) + (result.update?.length || 0) + (result.delete?.length || 0), + created: result.create?.length || 0, + updated: result.update?.length || 0, + deleted: result.delete?.length || 0, + errors: errors + }; } /** diff --git a/src/service/site.service.ts b/src/service/site.service.ts index 4585bf0..a8b8fe4 100644 --- a/src/service/site.service.ts +++ b/src/service/site.service.ts @@ -2,7 +2,6 @@ import { Provide, Scope, ScopeEnum } from '@midwayjs/core'; import { InjectEntityModel } from '@midwayjs/typeorm'; import { Repository, Like, In } from 'typeorm'; import { Site } from '../entity/site.entity'; -import { WpSite } from '../interface'; import { CreateSiteDTO, UpdateSiteDTO } from '../dto/site.dto'; import { Area } from '../entity/area.entity'; import { StockPoint } from '../entity/stock_point.entity'; @@ -19,29 +18,6 @@ export class SiteService { @InjectEntityModel(StockPoint) stockPointModel: Repository; - async syncFromConfig(sites: WpSite[] = []) { - // 将配置中的 WpSite 同步到数据库 Site 表(用于一次性导入或初始化) - for (const siteConfig of sites) { - // 按站点名称查询是否已存在记录 - const exist = await this.siteModel.findOne({ - where: { name: siteConfig.name }, - }); - // 将 WpSite 字段映射为 Site 实体字段 - const payload: Partial = { - name: siteConfig.name, - apiUrl: (siteConfig as any).wpApiUrl, - consumerKey: (siteConfig as any).consumerKey, - consumerSecret: (siteConfig as any).consumerSecret, - type: 'woocommerce', - }; - // 存在则更新,不存在则插入新记录 - if (exist) { - await this.siteModel.update({ id: exist.id }, payload); - } else { - await this.siteModel.insert(payload as Site); - } - } - } async create(data: CreateSiteDTO) { // 从 DTO 中分离出区域代码和其他站点数据 diff --git a/src/service/wp.service.ts b/src/service/wp.service.ts index 175d842..0c22b58 100644 --- a/src/service/wp.service.ts +++ b/src/service/wp.service.ts @@ -10,9 +10,10 @@ 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'; - +const MAX_PAGE_SIZE = 100; @Provide() export class WPService implements IPlatformService { getCustomer(site: any, id: number): Promise { @@ -79,11 +80,80 @@ export class WPService implements IPlatformService { /** * 通过 SDK 聚合分页数据,返回全部数据 + * 使用并发方式获取所有分页数据,提高性能 + * 默认按 date_created 倒序排列,确保获取最新的数据 */ - private async sdkGetAll(api: WooCommerceRestApi, resource: string, params: Record = {}, maxPages: number = 50): Promise { - // 直接传入较大的per_page参数,一次性获取所有数据 - const { items } = await this.sdkGetPage(api, resource, { ...params, per_page: 100 }); - return items; + private async sdkGetAll(api: WooCommerceRestApi, resource: string, params: Record = {}, maxPages: number = MAX_PAGE_SIZE): Promise { + return this.sdkGetAllConcurrent(api, resource, params, maxPages); + } + + /** + * 通过 SDK 聚合分页数据,使用并发方式获取所有分页数据 + * 支持自定义并发数和最大页数限制 + * 默认按 date_created 倒序排列,确保获取最新的数据 + */ + private async sdkGetAllConcurrent( + api: WooCommerceRestApi, + resource: string, + params: Record = {}, + maxPages: number = MAX_PAGE_SIZE, + concurrencyLimit: number = 5 + ): Promise { + // 设置默认排序为 date_created 倒序,确保获取最新数据 + const defaultParams = { + orderby: 'date_created', + order: 'desc', + per_page: MAX_PAGE_SIZE, + ...params + }; + + // 首先获取第一页数据,同时获取总页数信息 + const firstPage = await this.sdkGetPage(api, resource, { ...defaultParams, page: 1 }); + const { items: firstPageItems, totalPages } = firstPage; + + // 如果只有一页数据,直接返回 + if (totalPages <= 1) { + return firstPageItems; + } + + // 限制最大页数,避免过多的并发请求 + const actualMaxPages = Math.min(totalPages, maxPages); + + // 收集所有页面数据,从第二页开始 + const allItems = [...firstPageItems]; + let currentPage = 2; + + // 使用并发限制,避免一次性发起过多请求 + while (currentPage <= actualMaxPages) { + const batchPromises: Promise[] = []; + const batchSize = Math.min(concurrencyLimit, actualMaxPages - currentPage + 1); + + // 创建当前批次的并发请求 + for (let i = 0; i < batchSize; i++) { + const page = currentPage + i; + const pagePromise = this.sdkGetPage(api, resource, { ...defaultParams, page }) + .then(pageResult => pageResult.items) + .catch(error => { + console.error(`获取第 ${page} 页数据失败:`, error); + return []; // 如果某页获取失败,返回空数组,不影响整体结果 + }); + + batchPromises.push(pagePromise); + } + + // 等待当前批次完成 + const batchResults = await Promise.all(batchPromises); + + // 合并当前批次的数据 + for (const pageItems of batchResults) { + allItems.push(...pageItems); + } + + // 移动到下一批次 + currentPage += batchSize; + } + + return allItems; } /** @@ -551,12 +621,42 @@ export class WPService implements IPlatformService { */ async batchProcessProducts( site: any, - data: { create?: any[]; update?: any[]; delete?: any[] } - ): Promise { + data: BatchOperationDTO + ): Promise { const api = this.createApi(site, 'wc/v3'); try { const response = await api.post('products/batch', data); - return response.data; + const result = response.data; + + // 转换 WooCommerce 批量操作结果为统一格式 + const errors: Array<{identifier: string, error: string}> = []; + + // WooCommerce 返回格式: { create: [...], update: [...], delete: [...] } + // 错误信息可能在每个项目的 error 字段中 + const checkForErrors = (items: any[]) => { + items.forEach(item => { + if (item.error) { + errors.push({ + identifier: String(item.id || item.sku || 'unknown'), + error: typeof item.error === 'string' ? item.error : JSON.stringify(item.error) + }); + } + }); + }; + + // 检查每个操作类型的结果中的错误 + if (result.create) checkForErrors(result.create); + if (result.update) checkForErrors(result.update); + if (result.delete) checkForErrors(result.delete); + + return { + total: (data.create?.length || 0) + (data.update?.length || 0) + (data.delete?.length || 0), + processed: (result.create?.length || 0) + (result.update?.length || 0) + (result.delete?.length || 0), + created: result.create?.length || 0, + updated: result.update?.length || 0, + deleted: result.delete?.length || 0, + errors: errors + }; } catch (error) { console.error('批量处理产品失败:', error.response?.data || error.message); throw error; diff --git a/src/service/wp_product.service.ts b/src/service/wp_product.service.ts index 24fbaea..6b900e2 100644 --- a/src/service/wp_product.service.ts +++ b/src/service/wp_product.service.ts @@ -555,7 +555,7 @@ export class WpProductService { // 同步一个网站 async syncSite(siteId: number) { try { - // 通过数据库获取站点并转换为 WpSite,用于后续 WooCommerce 同步 + // 通过数据库获取站点并转换为 Site,用于后续 WooCommerce 同步 const site = await this.siteService.get(siteId, true); const externalProductIds = this.wpProductModel.createQueryBuilder('wp_product') .select([ -- 2.40.1 From 1628c8de41d984c908735959b1faea9381eaed3c Mon Sep 17 00:00:00 2001 From: tikkhun Date: Tue, 23 Dec 2025 19:33:12 +0800 Subject: [PATCH 3/8] =?UTF-8?q?feat(adapter):=20=E6=B7=BB=E5=8A=A0getAll?= =?UTF-8?q?=E6=96=B9=E6=B3=95=E6=94=AF=E6=8C=81=E6=89=B9=E9=87=8F=E8=8E=B7?= =?UTF-8?q?=E5=8F=96=E6=95=B0=E6=8D=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 实现WooCommerceAdapter中的getAllProducts、getAllOrders等方法 添加ISiteAdapter接口中的对应方法定义 更新customer.service使用getAllCustomers方法 --- .gitignore | 4 +- src/adapter/shopyy.adapter.ts | 37 +++++++++++++++- src/adapter/woocommerce.adapter.ts | 57 ++++++++++++++++++++++++- src/interface/site-adapter.interface.ts | 31 ++++++++++++++ src/service/customer.service.ts | 6 +-- src/service/wp.service.ts | 12 +++--- 6 files changed, 133 insertions(+), 14 deletions(-) diff --git a/.gitignore b/.gitignore index e2c1f24..b52a73d 100644 --- a/.gitignore +++ b/.gitignore @@ -16,6 +16,6 @@ yarn.lock **/config.local.ts container scripts -ai tmp_uploads/ -.trae \ No newline at end of file +.trae +docs \ No newline at end of file diff --git a/src/adapter/shopyy.adapter.ts b/src/adapter/shopyy.adapter.ts index 2568db1..b4837a0 100644 --- a/src/adapter/shopyy.adapter.ts +++ b/src/adapter/shopyy.adapter.ts @@ -321,6 +321,11 @@ export class ShopyyAdapter implements ISiteAdapter { }; } + async getAllProducts(params?: UnifiedSearchParamsDTO): Promise { + // Shopyy getAllProducts 暂未实现 + throw new Error('Shopyy getAllProducts 暂未实现'); + } + async getProduct(id: string | number): Promise { // 使用ShopyyService获取单个产品 const product = await this.shopyyService.getProduct(this.site, id); @@ -385,6 +390,11 @@ export class ShopyyAdapter implements ISiteAdapter { }; } + async getAllOrders(params?: UnifiedSearchParamsDTO): Promise { + // Shopyy getAllOrders 暂未实现 + throw new Error('Shopyy getAllOrders 暂未实现'); + } + async getOrder(id: string | number): Promise { const data = await this.shopyyService.getOrder(String(this.site.id), String(id)); return this.mapOrder(data); @@ -478,6 +488,11 @@ export class ShopyyAdapter implements ISiteAdapter { throw new Error('Shopyy does not support subscriptions.'); } + async getAllSubscriptions(params?: UnifiedSearchParamsDTO): Promise { + // Shopyy getAllSubscriptions 暂未实现 + throw new Error('Shopyy getAllSubscriptions 暂未实现'); + } + async getMedia( params: UnifiedSearchParamsDTO ): Promise> { @@ -488,7 +503,7 @@ export class ShopyyAdapter implements ISiteAdapter { requestParams ); return { - items: items.map(this.mapMedia), + items: items.map(this.mapMedia.bind(this)), total, totalPages, page, @@ -496,6 +511,11 @@ export class ShopyyAdapter implements ISiteAdapter { }; } + async getAllMedia(params?: UnifiedSearchParamsDTO): Promise { + // Shopyy getAllMedia 暂未实现 + throw new Error('Shopyy getAllMedia 暂未实现'); + } + async createMedia(file: any): Promise { const createdMedia = await this.shopyyService.createMedia(this.site, file); return this.mapMedia(createdMedia); @@ -527,6 +547,11 @@ export class ShopyyAdapter implements ISiteAdapter { }; } + async getAllReviews(params?: UnifiedSearchParamsDTO): Promise { + // Shopyy getAllReviews 暂未实现 + throw new Error('Shopyy getAllReviews 暂未实现'); + } + async getReview(id: string | number): Promise { const review = await this.shopyyService.getReview(this.site, id); return this.mapReview(review); @@ -607,6 +632,11 @@ export class ShopyyAdapter implements ISiteAdapter { }; } + async getAllWebhooks(params?: UnifiedSearchParamsDTO): Promise { + // Shopyy getAllWebhooks 暂未实现 + throw new Error('Shopyy getAllWebhooks 暂未实现'); + } + async getWebhook(id: string | number): Promise { const webhook = await this.shopyyService.getWebhook(this.site, id); return this.mapWebhook(webhook); @@ -657,6 +687,11 @@ export class ShopyyAdapter implements ISiteAdapter { }; } + async getAllCustomers(params?: UnifiedSearchParamsDTO): Promise { + // Shopyy getAllCustomers 暂未实现 + throw new Error('Shopyy getAllCustomers 暂未实现'); + } + async getCustomer(id: string | number): Promise { const customer = await this.shopyyService.getCustomer(this.site, id); return this.mapCustomer(customer); diff --git a/src/adapter/woocommerce.adapter.ts b/src/adapter/woocommerce.adapter.ts index 9b200cf..9c5260b 100644 --- a/src/adapter/woocommerce.adapter.ts +++ b/src/adapter/woocommerce.adapter.ts @@ -1,5 +1,4 @@ import { ISiteAdapter } from '../interface/site-adapter.interface'; -import { IPlatformService } from '../interface/platform.interface'; import { UnifiedMediaDTO, UnifiedOrderDTO, @@ -26,10 +25,11 @@ import { WooProductSearchParams, } from '../dto/woocommerce.dto'; import { Site } from '../entity/site.entity'; +import { WPService } from '../service/wp.service'; export class WooCommerceAdapter implements ISiteAdapter { // 构造函数接收站点配置与服务实例 - constructor(private site: Site, private wpService: IPlatformService) { + constructor(private site: Site, private wpService: WPService) { this.mapProduct = this.mapProduct.bind(this); this.mapReview = this.mapReview.bind(this); this.mapCustomer = this.mapCustomer.bind(this); @@ -71,6 +71,17 @@ export class WooCommerceAdapter implements ISiteAdapter { } } + // 获取所有webhooks + async getAllWebhooks(params?: UnifiedSearchParamsDTO): Promise { + try { + const api = (this.wpService as any).createApi(this.site, 'wc/v3'); + const webhooks = await this.wpService.sdkGetAll(api, 'webhooks', params); + return webhooks.map((webhook: any) => this.mapWebhook(webhook)); + } catch (error) { + throw new Error(`Failed to get all webhooks: ${error instanceof Error ? error.message : String(error)}`); + } + } + // 获取单个 webhook 详情 async getWebhook(id: string | number): Promise { try { @@ -466,6 +477,13 @@ export class WooCommerceAdapter implements ISiteAdapter { }; } + async getAllProducts(params?: UnifiedSearchParamsDTO): Promise { + // 使用sdkGetAll获取所有产品数据,不受分页限制 + const api = (this.wpService as any).createApi(this.site, 'wc/v3'); + const products = await this.wpService.sdkGetAll(api, 'products', params); + return products.map((product: any) => this.mapProduct(product)); + } + async getProduct(id: string | number): Promise { // 获取单个产品详情并映射为统一产品DTO const api = (this.wpService as any).createApi(this.site, 'wc/v3'); @@ -546,6 +564,13 @@ export class WooCommerceAdapter implements ISiteAdapter { return this.mapOrder(res.data); } + async getAllOrders(params?: UnifiedSearchParamsDTO): Promise { + // 使用sdkGetAll获取所有订单数据,不受分页限制 + const api = (this.wpService as any).createApi(this.site, 'wc/v3'); + const orders = await this.wpService.sdkGetAll(api, 'orders', params); + return orders.map((order: any) => this.mapOrder(order)); + } + async createOrder(data: Partial): Promise { // 创建订单并返回统一订单DTO const api = (this.wpService as any).createApi(this.site, 'wc/v3'); @@ -649,6 +674,13 @@ export class WooCommerceAdapter implements ISiteAdapter { }; } + async getAllSubscriptions(params?: UnifiedSearchParamsDTO): Promise { + // 使用sdkGetAll获取所有订阅数据,不受分页限制 + const api = (this.wpService as any).createApi(this.site, 'wc/v3'); + const subscriptions = await this.wpService.sdkGetAll(api, 'subscriptions', params); + return subscriptions.map((subscription: any) => this.mapSubscription(subscription)); + } + async getMedia( params: UnifiedSearchParamsDTO ): Promise> { @@ -666,6 +698,13 @@ export class WooCommerceAdapter implements ISiteAdapter { }; } + async getAllMedia(params?: UnifiedSearchParamsDTO): Promise { + // 使用sdkGetAll获取所有媒体数据,不受分页限制 + const api = (this.wpService as any).createApi(this.site, 'wc/v3'); + const media = await this.wpService.sdkGetAll(api, 'media', params); + return media.map((mediaItem: any) => this.mapMedia(mediaItem)); + } + private mapReview(item: any): UnifiedReviewDTO & {raw: any} { // 将 WooCommerce 评论数据映射为统一评论DTO return { @@ -701,6 +740,13 @@ export class WooCommerceAdapter implements ISiteAdapter { }; } + async getAllReviews(params?: UnifiedSearchParamsDTO): Promise { + // 使用sdkGetAll获取所有评论数据,不受分页限制 + const api = (this.wpService as any).createApi(this.site, 'wc/v3'); + const reviews = await this.wpService.sdkGetAll(api, 'products/reviews', params); + return reviews.map((review: any) => this.mapReview(review)); + } + async createReview(data: any): Promise { const res = await this.wpService.createReview(this.site, data); return this.mapReview(res); @@ -769,6 +815,13 @@ export class WooCommerceAdapter implements ISiteAdapter { }; } + async getAllCustomers(params?: UnifiedSearchParamsDTO): Promise { + // 使用sdkGetAll获取所有客户数据,不受分页限制 + const api = (this.wpService as any).createApi(this.site, 'wc/v3'); + const customers = await this.wpService.sdkGetAll(api, 'customers', params); + return customers.map((customer: any) => this.mapCustomer(customer)); + } + async getCustomer(id: string | number): Promise { const api = (this.wpService as any).createApi(this.site, 'wc/v3'); const res = await api.get(`customers/${id}`); diff --git a/src/interface/site-adapter.interface.ts b/src/interface/site-adapter.interface.ts index 0ae0179..b480ea6 100644 --- a/src/interface/site-adapter.interface.ts +++ b/src/interface/site-adapter.interface.ts @@ -22,6 +22,11 @@ export interface ISiteAdapter { */ getProducts(params: UnifiedSearchParamsDTO): Promise>; + /** + * 获取所有产品 + */ + getAllProducts(params?: UnifiedSearchParamsDTO): Promise; + /** * 获取单个产品 */ @@ -32,6 +37,11 @@ export interface ISiteAdapter { */ getOrders(params: UnifiedSearchParamsDTO): Promise>; + /** + * 获取所有订单 + */ + getAllOrders(params?: UnifiedSearchParamsDTO): Promise; + /** * 获取单个订单 */ @@ -42,11 +52,21 @@ export interface ISiteAdapter { */ getSubscriptions(params: UnifiedSearchParamsDTO): Promise>; + /** + * 获取所有订阅 + */ + getAllSubscriptions(params?: UnifiedSearchParamsDTO): Promise; + /** * 获取媒体列表 */ getMedia(params: UnifiedSearchParamsDTO): Promise>; + /** + * 获取所有媒体 + */ + getAllMedia(params?: UnifiedSearchParamsDTO): Promise; + /** * 创建媒体 */ @@ -57,6 +77,11 @@ export interface ISiteAdapter { */ getReviews(params: UnifiedSearchParamsDTO): Promise>; + /** + * 获取所有评论 + */ + getAllReviews(params?: UnifiedSearchParamsDTO): Promise; + /** * 创建评论 */ @@ -111,6 +136,7 @@ export interface ISiteAdapter { batchProcessOrders?(data: BatchOperationDTO): Promise; getCustomers(params: UnifiedSearchParamsDTO): Promise>; + getAllCustomers(params?: UnifiedSearchParamsDTO): Promise; getCustomer(id: string | number): Promise; createCustomer(data: Partial): Promise; updateCustomer(id: string | number, data: Partial): Promise; @@ -123,6 +149,11 @@ export interface ISiteAdapter { */ getWebhooks(params: UnifiedSearchParamsDTO): Promise; + /** + * 获取所有webhooks + */ + getAllWebhooks(params?: UnifiedSearchParamsDTO): Promise; + /** * 获取单个webhook */ diff --git a/src/service/customer.service.ts b/src/service/customer.service.ts index 76bb4fb..7d28467 100644 --- a/src/service/customer.service.ts +++ b/src/service/customer.service.ts @@ -171,17 +171,17 @@ export class CustomerService { try { // 第一步:获取适配器并从站点获取客户数据 const adapter = await this.siteApiService.getAdapter(siteId); - const siteCustomersResult = await adapter.getCustomers(params || {}); + const siteCustomers = await adapter.getAllCustomers(params || {}); // 第二步:将站点客户数据转换为客户实体数据 - const customersData = siteCustomersResult.items.map(siteCustomer => { + const customersData = siteCustomers.map(siteCustomer => { return this.mapSiteCustomerToCustomer(siteCustomer, siteId); }); // 第三步:批量upsert客户数据 const upsertResult = await this.upsertManyCustomers(customersData); return { - total: siteCustomersResult.total, + total: siteCustomers.length, processed: upsertResult.customers.length, synced: upsertResult.customers.length, updated: upsertResult.updated, diff --git a/src/service/wp.service.ts b/src/service/wp.service.ts index 0c22b58..f3e35b7 100644 --- a/src/service/wp.service.ts +++ b/src/service/wp.service.ts @@ -16,12 +16,13 @@ import * as fs from 'fs'; const MAX_PAGE_SIZE = 100; @Provide() export class WPService implements IPlatformService { - getCustomer(site: any, id: number): Promise { - throw new Error('Method not implemented.'); - } + @Inject() private readonly siteService: SiteService; + getCustomer(site: any, id: number): Promise { + throw new Error('Method not implemented.'); + } /** * 构建 URL,自动规范各段的斜杠,避免出现多 / 或少 / 导致请求失败 * 使用示例:this.buildURL(wpApiUrl, '/wp-json', 'wc/v3/products', productId) @@ -83,7 +84,7 @@ export class WPService implements IPlatformService { * 使用并发方式获取所有分页数据,提高性能 * 默认按 date_created 倒序排列,确保获取最新的数据 */ - private async sdkGetAll(api: WooCommerceRestApi, resource: string, params: Record = {}, maxPages: number = MAX_PAGE_SIZE): Promise { + async sdkGetAll(api: WooCommerceRestApi, resource: string, params: Record = {}, maxPages: number = MAX_PAGE_SIZE): Promise { return this.sdkGetAllConcurrent(api, resource, params, maxPages); } @@ -101,8 +102,7 @@ export class WPService implements IPlatformService { ): Promise { // 设置默认排序为 date_created 倒序,确保获取最新数据 const defaultParams = { - orderby: 'date_created', - order: 'desc', + order: 'desc', // 倒序,优先获取最新数据 per_page: MAX_PAGE_SIZE, ...params }; -- 2.40.1 From 16cd48e5030bc60062892dcd6549b53a30c4e731 Mon Sep 17 00:00:00 2001 From: tikkhun Date: Wed, 24 Dec 2025 11:04:51 +0800 Subject: [PATCH 4/8] =?UTF-8?q?refactor(service):=20=E7=A7=BB=E9=99=A4?= =?UTF-8?q?=E6=9F=A5=E8=AF=A2=E4=B8=AD=E6=9C=AA=E4=BD=BF=E7=94=A8=E7=9A=84?= =?UTF-8?q?tags=E5=AD=97=E6=AE=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/service/customer.service.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/service/customer.service.ts b/src/service/customer.service.ts index 7d28467..1b9cf00 100644 --- a/src/service/customer.service.ts +++ b/src/service/customer.service.ts @@ -370,8 +370,7 @@ export class CustomerService { 'c.created_at', 'c.updated_at', 'c.site_created_at', - 'c.site_updated_at', - 'GROUP_CONCAT(ct.tag) as tags' + 'c.site_updated_at' ]) .groupBy('c.id'); -- 2.40.1 From 677b11c48fa0a35d7c3a1e996a3f79f2a7ab1fe7 Mon Sep 17 00:00:00 2001 From: tikkhun Date: Wed, 24 Dec 2025 14:50:56 +0800 Subject: [PATCH 5/8] =?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 4686fe2..632b8e3 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; @@ -1618,86 +1613,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 -- 2.40.1 From aa4d3ef8307d0390e920d420c451f2aa7fe52092 Mon Sep 17 00:00:00 2001 From: tikkhun Date: Wed, 24 Dec 2025 15:44:45 +0800 Subject: [PATCH 6/8] =?UTF-8?q?chore:=20=E8=BF=98=E5=8E=9F=20config.local.?= =?UTF-8?q?ts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/config/config.local.ts | 42 ++------------------------------------ 1 file changed, 2 insertions(+), 40 deletions(-) diff --git a/src/config/config.local.ts b/src/config/config.local.ts index 0c962ce..6d381aa 100644 --- a/src/config/config.local.ts +++ b/src/config/config.local.ts @@ -16,9 +16,9 @@ export default { dataSource: { default: { host: 'localhost', - port: "23306", + port: "3306", username: 'root', - password: '12345678', + password: 'root', database: 'inventory', }, }, @@ -33,44 +33,6 @@ export default { secret: 'YOONE2024!@abc', expiresIn: '7d', }, - wpSite: [ - { - id: '200', - wpApiUrl: "http://simple.local", - consumerKey: 'ck_11b446d0dfd221853830b782049cf9a17553f886', - consumerSecret: 'cs_2b06729269f659dcef675b8cdff542bf3c1da7e8', - name: 'LocalSimple', - email: '2469687281@qq.com', - emailPswd: 'lulin91.', - }, - // { - // id: '2', - // wpApiUrl: 'http://t2-shop.local/', - // consumerKey: 'ck_a369473a6451dbaec63d19cbfd74a074b2c5f742', - // consumerSecret: 'cs_0946bbbeea1bfefff08a69e817ac62a48412df8c', - // name: 'Local', - // email: '2469687281@qq.com', - // emailPswd: 'lulin91.', - // }, - // { - // id: '3', - // wpApiUrl: 'http://t1-shop.local/', - // consumerKey: 'ck_a369473a6451dbaec63d19cbfd74a074b2c5f742', - // consumerSecret: 'cs_0946bbbeea1bfefff08a69e817ac62a48412df8c', - // name: 'Local-test-2', - // email: '2469687281@qq.com', - // emailPswd: 'lulin91.', - // }, - // { - // id: '2', - // wpApiUrl: 'http://localhost:10004', - // consumerKey: 'ck_dc9e151e9048c8ed3e27f35ac79d2bf7d6840652', - // consumerSecret: 'cs_d05d625d7b0ac05c6d765671d8417f41d9477e38', - // name: 'Local', - // email: 'tom@yoonevape.com', - // emailPswd: 'lulin91.', - // }, - ], freightcom: { url: 'https://customer-external-api.ssd-test.freightcom.com', token: '6zGj1qPTL1jIkbLmgaiYc6SwHUIXJ2t25htUF8uuFYiCg8ILCY6xnBEbvrX1p79L', -- 2.40.1 From 0d2507838076fd2679508d5a57d42e9a301a3cae Mon Sep 17 00:00:00 2001 From: tikkhun Date: Wed, 24 Dec 2025 15:50:13 +0800 Subject: [PATCH 7/8] =?UTF-8?q?refactor:=20=E7=A7=BB=E9=99=A4Variation?= =?UTF-8?q?=E5=AE=9E=E4=BD=93=E5=8F=8A=E7=9B=B8=E5=85=B3=E5=BC=95=E7=94=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 删除Variation实体及其在服务、DTO和配置中的引用 添加产品表重命名的迁移脚本 --- src/config/config.default.ts | 2 - ...1765358400000-update-product-table-name.ts | 91 +++++++++++++++ src/dto/woocommerce.dto.ts | 3 +- src/entity/order_sale.entity.ts | 4 + src/entity/variation.entity.ts | 104 ------------------ src/service/order.service.ts | 4 - src/service/product.service.ts | 4 - src/service/wp.service.ts | 7 +- 8 files changed, 99 insertions(+), 120 deletions(-) create mode 100644 src/db/migrations/1765358400000-update-product-table-name.ts delete mode 100644 src/entity/variation.entity.ts diff --git a/src/config/config.default.ts b/src/config/config.default.ts index de114e0..9d86ff2 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 { Variation } from '../entity/variation.entity'; import { User } from '../entity/user.entity'; import { PurchaseOrder } from '../entity/purchase_order.entity'; import { PurchaseOrderItem } from '../entity/purchase_order_item.entity'; @@ -52,7 +51,6 @@ export default { Product, ProductStockComponent, ProductSiteSku, - Variation, User, PurchaseOrder, PurchaseOrderItem, diff --git a/src/db/migrations/1765358400000-update-product-table-name.ts b/src/db/migrations/1765358400000-update-product-table-name.ts new file mode 100644 index 0000000..51deefd --- /dev/null +++ b/src/db/migrations/1765358400000-update-product-table-name.ts @@ -0,0 +1,91 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class UpdateProductTableName1765358400000 implements MigrationInterface { + name = 'UpdateProductTableName1765358400000' + + public async up(queryRunner: QueryRunner): Promise { + // 1. 使用 try-catch 方式删除外键约束,避免因外键不存在导致迁移失败 + try { + await queryRunner.query("ALTER TABLE `product_attributes_dict_item` DROP FOREIGN KEY `FK_592cdbdaebfec346c202ffb82ca`"); + } catch (error) { + // 忽略外键不存在的错误 + console.log('Warning: Failed to drop foreign key on product_attributes_dict_item. It may not exist.'); + } + + try { + await queryRunner.query("ALTER TABLE `product_stock_component` DROP FOREIGN KEY `FK_6fe75663083f572a49e7f46909b`"); + } catch (error) { + console.log('Warning: Failed to drop foreign key on product_stock_component. It may not exist.'); + } + + try { + await queryRunner.query("ALTER TABLE `product_site_sku` DROP FOREIGN KEY `FK_3b9b7f3d8a6d9f3e2c0b1a4d5e6`"); + } catch (error) { + console.log('Warning: Failed to drop foreign key on product_site_sku. It may not exist.'); + } + + try { + await queryRunner.query("ALTER TABLE `order_sale` DROP FOREIGN KEY `FK_order_sale_product`"); + } catch (error) { + console.log('Warning: Failed to drop foreign key on order_sale. It may not exist.'); + } + + // 2. 将 product 表重命名为 product_v2 + await queryRunner.query("ALTER TABLE `product` RENAME TO `product_v2`"); + + // 3. 重新创建所有外键约束,引用新的 product_v2 表 + await queryRunner.query("ALTER TABLE `product_attributes_dict_item` ADD CONSTRAINT `FK_592cdbdaebfec346c202ffb82ca` FOREIGN KEY (`productId`) REFERENCES `product_v2`(`id`) ON DELETE CASCADE ON UPDATE CASCADE"); + await queryRunner.query("ALTER TABLE `product_stock_component` ADD CONSTRAINT `FK_6fe75663083f572a49e7f46909b` FOREIGN KEY (`productId`) REFERENCES `product_v2`(`id`) ON DELETE CASCADE"); + await queryRunner.query("ALTER TABLE `product_site_sku` ADD CONSTRAINT `FK_3b9b7f3d8a6d9f3e2c0b1a4d5e6` FOREIGN KEY (`productId`) REFERENCES `product_v2`(`id`) ON DELETE CASCADE"); + + // 4. 为 order_sale 表添加外键约束 + try { + await queryRunner.query("ALTER TABLE `order_sale` ADD CONSTRAINT `FK_order_sale_product` FOREIGN KEY (`productId`) REFERENCES `product_v2`(`id`) ON DELETE CASCADE"); + } catch (error) { + console.log('Warning: Failed to add foreign key on order_sale. It may already exist.'); + } + } + + public async down(queryRunner: QueryRunner): Promise { + // 回滚操作 + // 1. 删除外键约束 + try { + await queryRunner.query("ALTER TABLE `product_attributes_dict_item` DROP FOREIGN KEY `FK_592cdbdaebfec346c202ffb82ca`"); + } catch (error) { + console.log('Warning: Failed to drop foreign key on product_attributes_dict_item during rollback.'); + } + + try { + await queryRunner.query("ALTER TABLE `product_stock_component` DROP FOREIGN KEY `FK_6fe75663083f572a49e7f46909b`"); + } catch (error) { + console.log('Warning: Failed to drop foreign key on product_stock_component during rollback.'); + } + + try { + await queryRunner.query("ALTER TABLE `product_site_sku` DROP FOREIGN KEY `FK_3b9b7f3d8a6d9f3e2c0b1a4d5e6`"); + } catch (error) { + console.log('Warning: Failed to drop foreign key on product_site_sku during rollback.'); + } + + try { + await queryRunner.query("ALTER TABLE `order_sale` DROP FOREIGN KEY `FK_order_sale_product`"); + } catch (error) { + console.log('Warning: Failed to drop foreign key on order_sale during rollback.'); + } + + // 2. 将 product_v2 表重命名回 product + await queryRunner.query("ALTER TABLE `product_v2` RENAME TO `product`"); + + // 3. 重新创建外键约束,引用回原来的 product 表 + await queryRunner.query("ALTER TABLE `product_attributes_dict_item` ADD CONSTRAINT `FK_592cdbdaebfec346c202ffb82ca` FOREIGN KEY (`productId`) REFERENCES `product`(`id`) ON DELETE CASCADE ON UPDATE CASCADE"); + await queryRunner.query("ALTER TABLE `product_stock_component` ADD CONSTRAINT `FK_6fe75663083f572a49e7f46909b` FOREIGN KEY (`productId`) REFERENCES `product`(`id`) ON DELETE CASCADE"); + await queryRunner.query("ALTER TABLE `product_site_sku` ADD CONSTRAINT `FK_3b9b7f3d8a6d9f3e2c0b1a4d5e6` FOREIGN KEY (`productId`) REFERENCES `product`(`id`) ON DELETE CASCADE"); + + // 4. 为 order_sale 表重新创建外键约束 + try { + await queryRunner.query("ALTER TABLE `order_sale` ADD CONSTRAINT `FK_order_sale_product` FOREIGN KEY (`productId`) REFERENCES `product`(`id`) ON DELETE CASCADE"); + } catch (error) { + console.log('Warning: Failed to add foreign key on order_sale during rollback. It may already exist.'); + } + } +} \ No newline at end of file diff --git a/src/dto/woocommerce.dto.ts b/src/dto/woocommerce.dto.ts index 12e4ec2..9686ee5 100644 --- a/src/dto/woocommerce.dto.ts +++ b/src/dto/woocommerce.dto.ts @@ -1,7 +1,6 @@ // WooCommerce 平台原始数据类型定义 // 仅包含当前映射逻辑所需字段以保持简洁与类型安全 -import { Variation } from "../entity/variation.entity"; // 产品类型 export interface WooProduct { @@ -126,7 +125,7 @@ export interface WooProduct { // 元数据 meta_data?: Array<{ id?: number; key: string; value: any }>; } -export interface WooVariation extends Variation{ +export interface WooVariation{ } diff --git a/src/entity/order_sale.entity.ts b/src/entity/order_sale.entity.ts index eec088f..552a96c 100644 --- a/src/entity/order_sale.entity.ts +++ b/src/entity/order_sale.entity.ts @@ -9,10 +9,14 @@ import { PrimaryGeneratedColumn, UpdateDateColumn, } from 'typeorm'; +// import { Product } from './product.entity'; @Entity('order_sale') @Exclude() export class OrderSale { + // @ManyToOne(() => Product, { onDelete: 'CASCADE' }) + // @JoinColumn({ name: 'productId' }) + // product: Product; @ApiProperty() @PrimaryGeneratedColumn() @Expose() diff --git a/src/entity/variation.entity.ts b/src/entity/variation.entity.ts deleted file mode 100644 index b3232f5..0000000 --- a/src/entity/variation.entity.ts +++ /dev/null @@ -1,104 +0,0 @@ -import { - Entity, - Column, - PrimaryGeneratedColumn, - Unique, - CreateDateColumn, - UpdateDateColumn, -} from 'typeorm'; -import { ApiProperty } from '@midwayjs/swagger'; - -@Entity('variation') -@Unique(['siteId', 'externalProductId', 'externalVariationId']) // 确保变体的唯一性 -export class Variation { - @ApiProperty({ - example: '1', - description: 'ID', - type: 'number', - required: true, - }) - @PrimaryGeneratedColumn() - id: number; - - @ApiProperty({ - description: '站点 id', - example: 1, - required: false - }) - @Column({ nullable: true }) - siteId: number; - - @ApiProperty({ - example: '1', - description: 'wp产品ID', - type: 'string', - required: true, - }) - @Column() - externalProductId: string; // WooCommerce 产品 ID - - @ApiProperty({ - example: '1', - description: 'wp变体ID', - type: 'string', - required: true, - }) - @Column() - externalVariationId: string; // WooCommerce 变体 ID - - @ApiProperty({ - example: '1', - description: '对应WP产品表的ID', - type: 'number', - required: true, - }) - @Column() - productId: number; // 对应WP产品表的 ID - - @ApiProperty({ description: 'sku', type: 'string' }) - @Column({ nullable: true }) - sku?: string; // sku 编码 - - @ApiProperty({ - description: '变体名称', - type: 'string', - required: true, - }) - @Column() - name: string; - - @ApiProperty({ description: '常规价格', type: Number }) - @Column('decimal', { precision: 10, scale: 2, nullable: true }) - regular_price: number; // 常规价格 - - @ApiProperty({ description: '销售价格', type: Number }) - @Column('decimal', { precision: 10, scale: 2, nullable: true }) - sale_price: number; // 销售价格 - - @ApiProperty({ description: '是否促销中', type: Boolean }) - @Column({ nullable: true, type: Boolean }) - on_sale: boolean; // 是否促销中 - - @ApiProperty({ description: '是否删除', type: Boolean }) - @Column({ nullable: true, type: Boolean , default: false }) - on_delete: boolean; // 是否删除 - - @Column({ type: 'json', nullable: true }) - attributes: Record; // 变体的属性 - - @ApiProperty({ - example: '2022-12-12 11:11:11', - description: '创建时间', - required: true, - }) - @CreateDateColumn() - createdAt: Date; - - @ApiProperty({ - example: '2022-12-12 11:11:11', - description: '更新时间', - required: true, - }) - @UpdateDateColumn() - updatedAt: Date; -} diff --git a/src/service/order.service.ts b/src/service/order.service.ts index 632b8e3..3df7e5b 100644 --- a/src/service/order.service.ts +++ b/src/service/order.service.ts @@ -20,7 +20,6 @@ import { OrderStatus, StockRecordOperationType, } from '../enums/base.enum'; -import { Variation } from '../entity/variation.entity'; import { CreateOrderNoteDTO, QueryOrderSalesDTO } from '../dto/order.dto'; import dayjs = require('dayjs'); import { OrderDetailRes } from '../dto/reponse.dto'; @@ -56,9 +55,6 @@ export class OrderService { @InjectEntityModel(OrderSale) orderSaleModel: Repository; - @InjectEntityModel(Variation) - variationModel: Repository; - @InjectEntityModel(Product) productModel: Repository; diff --git a/src/service/product.service.ts b/src/service/product.service.ts index 40e5b5f..1ea6e34 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 { Variation } from '../entity/variation.entity'; import { Dict } from '../entity/dict.entity'; import { DictItem } from '../entity/dict_item.entity'; import { Context } from '@midwayjs/koa'; @@ -52,9 +51,6 @@ export class ProductService { @InjectEntityModel(DictItem) dictItemModel: Repository; - @InjectEntityModel(Variation) - variationModel: Repository; - @InjectEntityModel(Stock) stockModel: Repository; diff --git a/src/service/wp.service.ts b/src/service/wp.service.ts index c99e66c..9eff41c 100644 --- a/src/service/wp.service.ts +++ b/src/service/wp.service.ts @@ -5,7 +5,6 @@ import { Inject, Provide } from '@midwayjs/core'; import axios, { AxiosRequestConfig } from 'axios'; import WooCommerceRestApi, { WooCommerceRestApiVersion } from '@woocommerce/woocommerce-rest-api'; -import { Variation } from '../entity/variation.entity'; import { SiteService } from './site.service'; import { IPlatformService } from '../interface/platform.interface'; import { BatchOperationDTO, BatchOperationResultDTO } from '../dto/batch.dto'; @@ -264,17 +263,17 @@ export class WPService implements IPlatformService { async getVariations(site: any, productId: number, page: number = 1, pageSize: number = 100): Promise { const api = this.createApi(site, 'wc/v3'); - return await this.sdkGetPage(api, `products/${productId}/variations`, { page, per_page: pageSize }); + return await this.sdkGetPage(api, `products/${productId}/variations`, { page, per_page: pageSize }); } async getVariation( site: any, productId: number, variationId: number - ): Promise { + ): Promise { const api = this.createApi(site, 'wc/v3'); const res = await api.get(`products/${productId}/variations/${variationId}`); - return res.data as Variation; + return res.data as WooVariation; } async getOrder( -- 2.40.1 From b8dc3359326194f568c92e68c7b3738e06a888e4 Mon Sep 17 00:00:00 2001 From: tikkhun Date: Wed, 24 Dec 2025 15:55:36 +0800 Subject: [PATCH 8/8] =?UTF-8?q?refactor(entity):=20=E5=B0=86site=E5=AE=9E?= =?UTF-8?q?=E4=BD=93=E8=A1=A8=E5=90=8D=E6=94=B9=E4=B8=BAsite=5Fv2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/entity/site.entity.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/entity/site.entity.ts b/src/entity/site.entity.ts index 7e03f4b..9e6db2d 100644 --- a/src/entity/site.entity.ts +++ b/src/entity/site.entity.ts @@ -2,7 +2,7 @@ import { Column, Entity, JoinTable, ManyToMany, PrimaryGeneratedColumn } from 't import { Area } from './area.entity'; import { StockPoint } from './stock_point.entity'; -@Entity('site') +@Entity('site_v2') export class Site { @PrimaryGeneratedColumn() id: number; -- 2.40.1