diff --git a/src/config/config.default.ts b/src/config/config.default.ts index fb7e085..fd99d08 100644 --- a/src/config/config.default.ts +++ b/src/config/config.default.ts @@ -33,6 +33,7 @@ import { Customer } from '../entity/customer.entity'; import { DeviceWhitelist } from '../entity/device_whitelist'; import { AuthCode } from '../entity/auth_code'; import { Subscription } from '../entity/subscription.entity'; +import { Site } from '../entity/site.entity'; export default { // use for cookie sign key, should change to your own and keep security @@ -74,6 +75,7 @@ export default { DeviceWhitelist, AuthCode, Subscription, + Site, ], synchronize: true, logging: false, diff --git a/src/configuration.ts b/src/configuration.ts index 8205c4b..e413b12 100644 --- a/src/configuration.ts +++ b/src/configuration.ts @@ -17,6 +17,7 @@ import * as crossDomain from '@midwayjs/cross-domain'; import * as cron from '@midwayjs/cron'; import * as jwt from '@midwayjs/jwt'; import { USER_KEY } from './decorator/user.decorator'; +import { SiteService } from './service/site.service'; import { AuthMiddleware } from './middleware/auth.middleware'; @Configuration({ @@ -45,6 +46,9 @@ export class MainConfiguration { @Inject() jwtService: jwt.JwtService; // 注入 JwtService 实例 + @Inject() + siteService: SiteService; + async onReady() { // add middleware this.app.useMiddleware([ReportMiddleware, AuthMiddleware]); @@ -74,5 +78,8 @@ export class MainConfiguration { } } ); + + const sites = this.app.getConfig('wpSite') || []; + await this.siteService.syncFromConfig(sites); } } diff --git a/src/controller/site.controller.ts b/src/controller/site.controller.ts index c84c7c2..5c848d5 100644 --- a/src/controller/site.controller.ts +++ b/src/controller/site.controller.ts @@ -1,25 +1,75 @@ -import { Config, Controller, Get } from '@midwayjs/core'; +import { Body, Controller, Get, Inject, Param, Put, Post, Query } from '@midwayjs/core'; import { ApiOkResponse } from '@midwayjs/swagger'; import { WpSitesResponse } from '../dto/reponse.dto'; -import { successResponse } from '../utils/response.util'; -import { WpSite } from '../interface'; +import { errorResponse, successResponse } from '../utils/response.util'; +import { SiteService } from '../service/site.service'; +import { CreateSiteDTO, DisableSiteDTO, QuerySiteDTO, UpdateSiteDTO } from '../dto/site.dto'; @Controller('/site') export class SiteController { - @Config('wpSite') - sites: WpSite[]; + @Inject() + siteService: SiteService; - @ApiOkResponse({ - description: '关联网站', - type: WpSitesResponse, - }) + @ApiOkResponse({ description: '关联网站', type: WpSitesResponse }) @Get('/all') async all() { - return successResponse( - this.sites.map(v => ({ - id: v.id, - siteName: v.siteName, - })) - ); + try { + const { items } = await this.siteService.list({ current: 1, pageSize: 1000, isDisabled: false }); + return successResponse(items.map((v: any) => ({ id: v.id, siteName: v.siteName }))); + } catch (error) { + return errorResponse(error?.message || '获取失败'); + } + } + + @Post('/create') + async create(@Body() body: CreateSiteDTO) { + try { + await this.siteService.create(body); + return successResponse(true); + } catch (error) { + return errorResponse(error?.message || '创建失败'); + } + } + + @Put('/update/:id') + async update(@Param('id') id: string, @Body() body: UpdateSiteDTO) { + try { + await this.siteService.update(Number(id), body); + return successResponse(true); + } catch (error) { + return errorResponse(error?.message || '更新失败'); + } + } + + @Get('/get/:id') + async get(@Param('id') id: string) { + try { + const data = await this.siteService.get(Number(id), false); + return successResponse(data); + } catch (error) { + return errorResponse(error?.message || '获取失败'); + } + } + + @Get('/list') + async list(@Query() query: QuerySiteDTO) { + try { + const data = await this.siteService.list(query, false); + return successResponse(data); + } catch (error) { + return errorResponse(error?.message || '获取失败'); + } + } + + // 批量查询改为使用 /site/list?ids=1,2,3 + + @Put('/disable/:id') + async disable(@Param('id') id: string, @Body() body: DisableSiteDTO) { + try { + await this.siteService.disable(Number(id), body.disabled); + return successResponse(true); + } catch (error) { + return errorResponse(error?.message || '更新失败'); + } } } diff --git a/src/controller/webhook.controller.ts b/src/controller/webhook.controller.ts index 76714c5..d15da14 100644 --- a/src/controller/webhook.controller.ts +++ b/src/controller/webhook.controller.ts @@ -1,4 +1,4 @@ -import { Config, HttpStatus, Inject } from '@midwayjs/core'; +import { HttpStatus, Inject } from '@midwayjs/core'; import { Controller, Post, @@ -11,8 +11,8 @@ import { Context } from '@midwayjs/koa'; import * as crypto from 'crypto'; import { WpProductService } from '../service/wp_product.service'; import { WPService } from '../service/wp.service'; +import { SiteService } from '../service/site.service'; import { OrderService } from '../service/order.service'; -import { WpSite } from '../interface'; @Controller('/webhook') export class WebhookController { @@ -30,8 +30,10 @@ export class WebhookController { @Inject() ctx: Context; - @Config('wpSite') - sites: WpSite[]; + @Inject() + private readonly siteService: SiteService; + + // 移除配置中的站点数组,来源统一改为数据库 @Get('/') async test() { @@ -47,9 +49,10 @@ export class WebhookController { const signature = header['x-wc-webhook-signature']; const topic = header['x-wc-webhook-topic']; const source = header['x-wc-webhook-source']; - let site = this.sites.find(item => item.id === siteId); + // 从数据库获取站点配置 + const site = await this.siteService.get(Number(siteId), true); - if (!site || !source.includes(site.wpApiUrl)) { + if (!site || !source.includes(site.apiUrl)) { console.log('domain not match'); return { code: HttpStatus.BAD_REQUEST, @@ -94,13 +97,13 @@ export class WebhookController { ? await this.wpApiService.getVariations(site, body.id) : []; await this.wpProductService.syncProductAndVariations( - site.id, + String(site.id), body, variations ); break; case 'product.deleted': - await this.wpProductService.delWpProduct(site.id, body.id); + await this.wpProductService.delWpProduct(String(site.id), body.id); break; case 'order.created': case 'order.updated': diff --git a/src/controller/wp_product.controller.ts b/src/controller/wp_product.controller.ts index e99bbaa..8623b1d 100644 --- a/src/controller/wp_product.controller.ts +++ b/src/controller/wp_product.controller.ts @@ -7,7 +7,6 @@ import { Query, Put, Body, - Config, } from '@midwayjs/core'; import { WpProductService } from '../service/wp_product.service'; import { errorResponse, successResponse } from '../utils/response.util'; @@ -20,22 +19,13 @@ import { UpdateWpProductDTO, } from '../dto/wp_product.dto'; import { WPService } from '../service/wp.service'; -import { WpSite } from '../interface'; +import { SiteService } from '../service/site.service'; import { ProductsRes, } from '../dto/reponse.dto'; @Controller('/wp_product') export class WpProductController { - @Inject() - wpService: WPService; - - @Config('wpSite') - sites: WpSite[]; - - getSite(id: string): WpSite { - let idx = this.sites.findIndex(item => item.id === id); - return this.sites[idx]; - } + // 移除控制器内的配置站点引用,统一由服务层处理站点数据 @Inject() private readonly wpProductService: WpProductService; @@ -43,6 +33,9 @@ export class WpProductController { @Inject() private readonly wpApiService: WPService; + @Inject() + private readonly siteService: SiteService; + @ApiOkResponse({ type: BooleanRes, }) @@ -127,7 +120,7 @@ export class WpProductController { if (isDuplicate) { return errorResponse('SKU已存在'); } - const site = await this.wpProductService.getSite(siteId); + const site = await this.siteService.get(Number(siteId), true); const result = await this.wpApiService.updateProduct( site, productId, @@ -167,7 +160,7 @@ export class WpProductController { if (isDuplicate) { return errorResponse('SKU已存在'); } - const site = await this.wpProductService.getSite(siteId); + const site = await this.siteService.get(Number(siteId), true); const result = await this.wpApiService.updateVariation( site, productId, diff --git a/src/db/seed/index.ts b/src/db/seed/index.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/dto/site.dto.ts b/src/dto/site.dto.ts index 5c31a27..17fcdd1 100644 --- a/src/dto/site.dto.ts +++ b/src/dto/site.dto.ts @@ -8,7 +8,7 @@ export class SiteConfig { @ApiProperty({ description: '站点 URL' }) @Rule(RuleType.string()) - wpApiUrl: string; + apiUrl: string; @ApiProperty({ description: '站点 rest key' }) @Rule(RuleType.string()) @@ -22,11 +22,61 @@ export class SiteConfig { @Rule(RuleType.string()) siteName: string; - @ApiProperty({ description: '站点邮箱' }) - @Rule(RuleType.string()) - email?: string; + @ApiProperty({ description: '平台类型', enum: ['woocommerce', 'shopyy'] }) + @Rule(RuleType.string().valid('woocommerce', 'shopyy')) + type: string; - @ApiProperty({ description: '站点邮箱密码' }) + @ApiProperty({ description: 'SKU 前缀' }) @Rule(RuleType.string()) - emailPswd?: string; + skuPrefix: string; +} + +export class CreateSiteDTO { + @Rule(RuleType.string().optional()) + apiUrl?: string; + @Rule(RuleType.string().optional()) + consumerKey?: string; + @Rule(RuleType.string().optional()) + consumerSecret?: string; + @Rule(RuleType.string()) + siteName: string; + @Rule(RuleType.string().valid('woocommerce', 'shopyy').optional()) + type?: string; + @Rule(RuleType.string().optional()) + skuPrefix?: string; +} + +export class UpdateSiteDTO { + @Rule(RuleType.string().optional()) + apiUrl?: string; + @Rule(RuleType.string().optional()) + consumerKey?: string; + @Rule(RuleType.string().optional()) + consumerSecret?: string; + @Rule(RuleType.string().optional()) + siteName?: string; + @Rule(RuleType.boolean().optional()) + isDisabled?: boolean; + @Rule(RuleType.string().valid('woocommerce', 'shopyy').optional()) + type?: string; + @Rule(RuleType.string().optional()) + skuPrefix?: string; +} + +export class QuerySiteDTO { + @Rule(RuleType.number().optional()) + current?: number; + @Rule(RuleType.number().optional()) + pageSize?: number; + @Rule(RuleType.string().optional()) + keyword?: string; + @Rule(RuleType.boolean().optional()) + isDisabled?: boolean; + @Rule(RuleType.string().optional()) + ids?: string; +} + +export class DisableSiteDTO { + @Rule(RuleType.boolean()) + disabled: boolean; } diff --git a/src/entity/site.entity.ts b/src/entity/site.entity.ts new file mode 100644 index 0000000..d4e3215 --- /dev/null +++ b/src/entity/site.entity.ts @@ -0,0 +1,28 @@ +import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm'; + +@Entity('site') +export class Site { + @PrimaryGeneratedColumn({ type: 'int' }) + id: number; + + @Column({ type: 'varchar', length: 255, nullable: true }) + apiUrl: string; + + @Column({ type: 'varchar', length: 255, nullable: true }) + consumerKey: string; + + @Column({ type: 'varchar', length: 255, nullable: true }) + consumerSecret: string; + + @Column({ type: 'varchar', length: 255, unique: true }) + siteName: string; + + @Column({ type: 'varchar', length: 32, default: 'woocommerce' }) + type: string; // 平台类型:woocommerce | shopyy + + @Column({ type: 'varchar', length: 64, nullable: true }) + skuPrefix: string; + + @Column({ type: 'tinyint', default: 0 }) + isDisabled: number; +} \ No newline at end of file diff --git a/src/enums/base.enum.ts b/src/enums/base.enum.ts index 1d18ee3..0dbe924 100644 --- a/src/enums/base.enum.ts +++ b/src/enums/base.enum.ts @@ -70,6 +70,7 @@ export enum ShipmentType { } export enum staticValue { + // 万能验证码 STATIC_CAPTCHA = 'yoone2025!@YOONE0923' } diff --git a/src/service/logistics.service.ts b/src/service/logistics.service.ts index 9f69895..a79503d 100644 --- a/src/service/logistics.service.ts +++ b/src/service/logistics.service.ts @@ -1,4 +1,4 @@ -import { Config, Inject, Provide, sleep } from '@midwayjs/core'; +import { Inject, Provide, sleep } from '@midwayjs/core'; import { InjectEntityModel, TypeORMDataSourceManager } from '@midwayjs/typeorm'; import { Service } from '../entity/service.entity'; import { In, IsNull, Like, Repository } from 'typeorm'; @@ -21,7 +21,6 @@ import { StockRecord } from '../entity/stock_record.entity'; import { Stock } from '../entity/stock.entity'; import { plainToClass } from 'class-transformer'; import { WPService } from './wp.service'; -import { WpSite } from '../interface'; // import { Product } from '../entity/product.entty'; import { ShippingDetailsDTO } from '../dto/freightcom.dto'; import { CanadaPostService } from './canadaPost.service'; @@ -31,6 +30,7 @@ import { UniExpressService } from './uni_express.service'; import { StockPoint } from '../entity/stock_point.entity'; import { OrderService } from './order.service'; import { convertKeysFromCamelToSnake } from '../utils/object-transform.util'; +import { SiteService } from './site.service'; @Provide() export class LogisticsService { @@ -82,13 +82,8 @@ export class LogisticsService { @Inject() dataSourceManager: TypeORMDataSourceManager; - @Config('wpSite') - sites: WpSite[]; - - getSite(id: string): WpSite { - let idx = this.sites.findIndex(item => item.id === id); - return this.sites[idx]; - } + @Inject() + private readonly siteService: SiteService; async getServiceList(param: QueryServiceDTO) { const { pageSize, current, carrier_name, isActive } = param; @@ -263,7 +258,7 @@ export class LogisticsService { try { // 同步订单状态到woocommerce - const site = await this.getSite(order.siteId); + const site = await this.siteService.get(Number(order.siteId), true); if (order.status === OrderStatus.COMPLETED) { await this.wpService.updateOrder(site, order.externalOrderId, { status: OrderStatus.PROCESSING, @@ -367,7 +362,7 @@ export class LogisticsService { const tracking_provider = 'UniUni'; // todo: id未确定,后写进常数 // 同步物流信息到woocommerce - const site = await this.getSite(order.siteId); + const site = await this.siteService.get(Number(order.siteId), true); const res = await this.wpService.createShipment(site, order.externalOrderId, { tracking_number: resShipmentOrder.data.tno, tracking_provider: tracking_provider, @@ -493,7 +488,7 @@ export class LogisticsService { const order = await this.orderModel.findOneBy({ id: orderShipment.order_id, }); - const site = this.getSite(order.siteId); + const site = await this.siteService.get(Number(order.siteId), true); await this.wpService.updateOrder(site, order.externalOrderId, { status: OrderStatus.COMPLETED, }); @@ -563,7 +558,10 @@ export class LogisticsService { }, }); - const siteMap = new Map(this.sites.map(site => [site.id, site.siteName])); + // 从数据库批量获取站点信息,构建映射以避免 N+1 查询 + const siteIds = Array.from(new Set(orders.map(o => o.siteId).filter(Boolean))); + const { items: sites } = await this.siteService.list({ current: 1, pageSize: 1000, ids: siteIds.join(',') }, false); + const siteMap = new Map(sites.map((s: any) => [String(s.id), s.siteName])); return orders.map(order => ({ ...order, diff --git a/src/service/order.service.ts b/src/service/order.service.ts index 7f7855c..d62baf9 100644 --- a/src/service/order.service.ts +++ b/src/service/order.service.ts @@ -1,5 +1,6 @@ -import { Config, Inject, Provide } from '@midwayjs/core'; +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'; @@ -27,7 +28,7 @@ import dayjs = require('dayjs'); import { OrderDetailRes } from '../dto/reponse.dto'; import { OrderNote } from '../entity/order_note.entity'; import { User } from '../entity/user.entity'; -import { WpSite } from '../interface'; +import { SiteService } from './site.service'; import { ShipmentItem } from '../entity/shipment_item.entity'; import { UpdateStockDTO } from '../dto/stock.dto'; import { StockService } from './stock.service'; @@ -35,8 +36,6 @@ import { OrderSaleOriginal } from '../entity/order_item_original.entity'; @Provide() export class OrderService { - @Config('wpSite') - sites: WpSite[]; @Inject() wpService: WPService; @@ -101,6 +100,9 @@ export class OrderService { @InjectEntityModel(Customer) customerModel: Repository; + @Inject() + siteService: SiteService; + async syncOrders(siteId: string) { const orders = await this.wpService.getOrders(siteId); // 调用 WooCommerce API 获取订单 for (const order of orders) { @@ -127,12 +129,9 @@ export class OrderService { return } try { - const site = this.sites.find(v => v.id === siteId); - if (!site) { - throw new Error(`更新订单信息,但失败,原因为 ${siteId} 的站点信息不存在`) - } - // 同步更新回 wordpress 的 order 状态 - await this.wpService.updateOrder(site, order.id, { status: order.status }); + const site = await this.siteService.get(siteId); + // 将订单状态同步到 WooCommerce,然后切换至下一状态 + await this.wpService.updateOrder(site, String(order.id), { status: order.status }); order.status = this.orderAutoNextStatusMap[originStatus]; } catch (error) { console.error('更新订单状态失败,原因为:', error) @@ -1262,7 +1261,7 @@ export class OrderService { } async getOrderDetail(id: number): Promise { const order = await this.orderModel.findOne({ where: { id } }); - const site = this.sites.find(site => site.id === order.siteId); + const site = await this.siteService.get(Number(order.siteId), true); const items = await this.orderItemModel.find({ where: { orderId: id } }); const sales = await this.orderSaleModel.find({ where: { orderId: id } }); const refunds = await this.orderRefundModel.find({ @@ -1352,7 +1351,8 @@ export class OrderService { return { ...order, siteName: site?.siteName, - email: site?.email, + // Site 实体无邮箱字段,这里返回空字符串保持兼容 + email: '', items, sales, refundItems, @@ -1415,20 +1415,22 @@ export class OrderService { ]), }, }); - return orders.map(order => { - return { - externalOrderId: order.externalOrderId, - id: order.id, - siteName: - this.sites.find(site => site.id === order.siteId)?.siteName || '', - }; - }); + // 批量获取订单涉及的站点名称,避免使用配置文件 + const siteIds = Array.from(new Set(orders.map(o => o.siteId).filter(Boolean))); + const { items: sites } = await this.siteService.list({ current: 1, pageSize: 1000, ids: siteIds.join(',') }, false); + const siteMap = new Map(sites.map((s: any) => [String(s.id), s.siteName])); + return orders.map(order => ({ + externalOrderId: order.externalOrderId, + id: order.id, + siteName: siteMap.get(order.siteId) || '', + })); } async cancelOrder(id: number) { const order = await this.orderModel.findOne({ where: { id } }); if (!order) throw new Error(`订单 ${id}不存在`); - const site = this.wpService.geSite(order.siteId); + 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, siteName: s.siteName, email: '', emailPswd: '' } as WpSite; if (order.status !== OrderStatus.CANCEL) { await this.wpService.updateOrder(site, order.externalOrderId, { status: OrderStatus.CANCEL, @@ -1442,7 +1444,7 @@ export class OrderService { async refundOrder(id: number) { const order = await this.orderModel.findOne({ where: { id } }); if (!order) throw new Error(`订单 ${id}不存在`); - const site = this.wpService.geSite(order.siteId); + const site = await this.siteService.get(Number(order.siteId), true); if (order.status !== OrderStatus.REFUNDED) { await this.wpService.updateOrder(site, order.externalOrderId, { status: OrderStatus.REFUNDED, @@ -1456,7 +1458,7 @@ export class OrderService { async completedOrder(id: number) { const order = await this.orderModel.findOne({ where: { id } }); if (!order) throw new Error(`订单 ${id}不存在`); - const site = this.wpService.geSite(order.siteId); + const site = await this.siteService.get(order.siteId); if (order.status !== OrderStatus.COMPLETED) { await this.wpService.updateOrder(site, order.externalOrderId, { status: OrderStatus.COMPLETED, diff --git a/src/service/site.service.ts b/src/service/site.service.ts new file mode 100644 index 0000000..c5f176b --- /dev/null +++ b/src/service/site.service.ts @@ -0,0 +1,96 @@ +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 { UpdateSiteDTO } from '../dto/site.dto'; + +@Provide() +@Scope(ScopeEnum.Singleton) +export class SiteService { + @InjectEntityModel(Site) + siteModel: Repository; + + async syncFromConfig(sites: WpSite[] = []) { + // 将配置中的 WpSite 同步到数据库 Site 表(用于一次性导入或初始化) + for (const siteConfig of sites) { + // 按站点名称查询是否已存在记录 + const exist = await this.siteModel.findOne({ where: { siteName: siteConfig.siteName } }); + // 将 WpSite 字段映射为 Site 实体字段 + const payload: Partial = { + siteName: siteConfig.siteName, + 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: Partial) { + // 创建新的站点记录 + await this.siteModel.insert(data as Site); + return true; + } + + async update(id: string | number, data: UpdateSiteDTO) { + // 更新指定站点记录,将布尔 isDisabled 转换为数值 0/1 + const payload: Partial = { + ...data, + isDisabled: + data.isDisabled === undefined // 未传入则不更新该字段 + ? undefined + : data.isDisabled // true -> 1, false -> 0 + ? 1 + : 0, + } as any; + await this.siteModel.update({ id: Number(id) }, payload); + return true; + } + + async get(id: string | number, includeSecret = false) { + // 根据主键获取站点;includeSecret 为 true 时返回密钥字段 + const site = await this.siteModel.findOne({ where: { id: Number(id) } }); + if (!site) return null; + if (includeSecret) return site; + // 默认不返回密钥,进行字段脱敏 + const { consumerKey, consumerSecret, ...rest } = site; + return rest; + } + + async list(param: { current?: number; pageSize?: number; keyword?: string; isDisabled?: boolean; ids?: string }, includeSecret = false) { + // 分页查询站点列表,支持关键字、禁用状态与 ID 列表过滤 + const { current = 1, pageSize = 10, keyword, isDisabled, ids } = (param || {}) as any; + const where: any = {}; + // 按名称模糊查询 + if (keyword) where.siteName = Like(`%${keyword}%`); + // 按禁用状态过滤(布尔转数值) + if (typeof isDisabled === 'boolean') where.isDisabled = isDisabled ? 1 : 0; + if (ids) { + // 解析逗号分隔的 ID 字符串为数字数组,并过滤非法值 + const numIds = String(ids) + .split(',') + .filter(Boolean) + .map((i) => Number(i)) + .filter((v) => !Number.isNaN(v)); + if (numIds.length > 0) where.id = In(numIds); + } + // 进行分页查询(skip/take)并返回总条数 + const [items, total] = await this.siteModel.findAndCount({ where, skip: (current - 1) * pageSize, take: pageSize }); + // 根据 includeSecret 决定是否脱敏返回密钥字段 + const data = includeSecret ? items : items.map((item: any) => { + const { consumerKey, consumerSecret, ...rest } = item; + return rest; + }); + return { items: data, total, current, pageSize }; + } + + async disable(id: string | number, disabled: boolean) { + // 设置站点禁用状态(true -> 1, false -> 0) + await this.siteModel.update({ id: Number(id) }, { isDisabled: disabled ? 1 : 0 }); + return true; + } +} \ No newline at end of file diff --git a/src/service/wp.service.ts b/src/service/wp.service.ts index ae91d7b..443018f 100644 --- a/src/service/wp.service.ts +++ b/src/service/wp.service.ts @@ -1,16 +1,16 @@ -import { Config, Provide } from '@midwayjs/core'; +import { Inject, Provide } from '@midwayjs/core'; import axios, { AxiosRequestConfig } from 'axios'; import WooCommerceRestApi, { WooCommerceRestApiVersion } from '@woocommerce/woocommerce-rest-api'; -import { WpSite } from '../interface'; import { WpProduct } from '../entity/wp_product.entity'; import { Variation } from '../entity/variation.entity'; import { UpdateVariationDTO, UpdateWpProductDTO } from '../dto/wp_product.dto'; import { ProductStatus, ProductStockStatus } from '../enums/base.enum'; +import { SiteService } from './site.service'; @Provide() export class WPService { - @Config('wpSite') - sites: WpSite[]; + @Inject() + private readonly siteService: SiteService; /** * 构建 URL,自动规范各段的斜杠,避免出现多 / 或少 / 导致请求失败 @@ -35,12 +35,11 @@ export class WPService { * @param site 站点配置 * @param namespace API 命名空间,默认 wc/v3;订阅推荐 wcs/v1 */ - private createApi(site: WpSite, namespace: WooCommerceRestApiVersion = 'wc/v3') { + private createApi(site: any, namespace: WooCommerceRestApiVersion = 'wc/v3') { return new WooCommerceRestApi({ - url: site.wpApiUrl, + url: site.apiUrl, consumerKey: site.consumerKey, consumerSecret: site.consumerSecret, - // SDK 的版本字段有联合类型限制,这里兼容插件命名空间(例如 wcs/v1) version: namespace, }); } @@ -52,15 +51,19 @@ export class WPService { const page = params.page ?? 1; const per_page = params.per_page ?? 100; const res = await api.get(resource.replace(/^\/+/, ''), { ...params, page, per_page }); + if (res?.headers?.['content-type']?.includes('text/html')) { + throw new Error('接口返回了 text/html,可能为 WordPress 登录页或错误页,请检查站点配置或权限'); + } const data = res.data as T[]; const totalPages = Number(res.headers?.['x-wp-totalpages'] ?? 1); - return { items: data, totalPages, page, per_page }; + const total = Number(res.headers?.['x-wp-total']?? 1) + return { items: data, total, totalPages, page, per_page }; } /** * 通过 SDK 聚合分页数据,返回全部数据 */ - private async sdkGetAll(api: any, resource: string, params: Record = {}, maxPages: number = 50): Promise { + private async sdkGetAll(api: WooCommerceRestApi, resource: string, params: Record = {}, maxPages: number = 50): Promise { const result: T[] = []; for (let page = 1; page <= maxPages; page++) { const { items, totalPages } = await this.sdkGetPage(api, resource, { ...params, page }); @@ -78,20 +81,18 @@ export class WPService { * @param consumerSecret WooCommerce 的消费者密钥 */ - geSite(id: string): WpSite { - let idx = this.sites.findIndex(item => item.id === id); - return this.sites[idx]; - } + async fetchData( endpoint: string, - site: WpSite, + site: any, param: Record = {} ): Promise { try { - const { wpApiUrl, consumerKey, consumerSecret } = site; + const apiUrl = site.apiUrl; + const { consumerKey, consumerSecret } = site; // 构建 URL,规避多/或少/问题 - const url = this.buildURL(wpApiUrl, '/wp-json', endpoint); + const url = this.buildURL(apiUrl, '/wp-json', endpoint); const auth = Buffer.from(`${consumerKey}:${consumerSecret}`).toString( 'base64' ); @@ -111,22 +112,22 @@ export class WPService { async fetchPagedData( endpoint: string, - site: WpSite, + site: any, page: number = 1, perPage: number = 100 ): Promise { const allData: T[] = []; - const { wpApiUrl, consumerKey, consumerSecret } = site; + const { apiUrl, consumerKey, consumerSecret } = site; const auth = Buffer.from(`${consumerKey}:${consumerSecret}`).toString( 'base64' ); - console.log(`!!!wpApiUrl, consumerKey, consumerSecret, auth`,wpApiUrl, consumerKey, consumerSecret, auth) + console.log(`!!!wpApiUrl, consumerKey, consumerSecret, auth`,site.apiUrl, consumerKey, consumerSecret, auth) let hasMore = true; while (hasMore) { const config: AxiosRequestConfig = { method: 'GET', // 构建 URL,规避多/或少/问题 - url: this.buildURL(wpApiUrl, '/wp-json', endpoint), + url: this.buildURL(apiUrl, '/wp-json', endpoint), headers: { Authorization: `Basic ${auth}`, }, @@ -156,18 +157,18 @@ export class WPService { return allData; } - async getProducts(site: WpSite): Promise { + async getProducts(site: any): Promise { const api = this.createApi(site, 'wc/v3'); return await this.sdkGetAll(api, 'products'); } - async getVariations(site: WpSite, productId: number): Promise { + async getVariations(site: any, productId: number): Promise { const api = this.createApi(site, 'wc/v3'); return await this.sdkGetAll(api, `products/${productId}/variations`); } async getVariation( - site: WpSite, + site: any, productId: number, variationId: number ): Promise { @@ -180,13 +181,13 @@ export class WPService { siteId: string, orderId: string ): Promise> { - const site = this.geSite(siteId); + const site = await this.siteService.get(siteId); const api = this.createApi(site, 'wc/v3'); const res = await api.get(`orders/${orderId}`); return res.data as Record; } async getOrders(siteId: string): Promise[]> { - const site = this.geSite(siteId); + const site = await this.siteService.get(siteId); const api = this.createApi(site, 'wc/v3'); return await this.sdkGetAll>(api, 'orders'); } @@ -197,7 +198,7 @@ export class WPService { * 返回所有分页合并后的订阅数组。 */ async getSubscriptions(siteId: string): Promise[]> { - const site = this.geSite(siteId); + const site = await this.siteService.get(siteId); // 优先使用 Subscriptions 命名空间 wcs/v1,失败回退 wc/v3 const api = this.createApi(site, 'wc/v3'); return await this.sdkGetAll>(api, 'subscriptions'); @@ -209,7 +210,7 @@ export class WPService { orderId: string, refundId: number ): Promise> { - const site = this.geSite(siteId); + const site = await this.siteService.get(siteId); const api = this.createApi(site, 'wc/v3'); const res = await api.get(`orders/${orderId}/refunds/${refundId}`); return res.data as Record; @@ -219,7 +220,7 @@ export class WPService { siteId: string, orderId: number ): Promise[]> { - const site = this.geSite(siteId); + const site = await this.siteService.get(siteId); const api = this.createApi(site, 'wc/v3'); return await this.sdkGetAll>(api, `orders/${orderId}/refunds`); } @@ -229,7 +230,7 @@ export class WPService { orderId: number, noteId: number ): Promise> { - const site = this.geSite(siteId); + const site = await this.siteService.get(siteId); const api = this.createApi(site, 'wc/v3'); const res = await api.get(`orders/${orderId}/notes/${noteId}`); return res.data as Record; @@ -239,24 +240,25 @@ export class WPService { siteId: string, orderId: number ): Promise[]> { - const site = this.geSite(siteId); + const site = await this.siteService.get(siteId); const api = this.createApi(site, 'wc/v3'); return await this.sdkGetAll>(api, `orders/${orderId}/notes`); } async updateData( endpoint: string, - site: WpSite, + site: any, data: Record ): Promise { - const { wpApiUrl, consumerKey, consumerSecret } = site; + const apiUrl = site.apiUrl; + const { consumerKey, consumerSecret } = site; const auth = Buffer.from(`${consumerKey}:${consumerSecret}`).toString( 'base64' ); const config: AxiosRequestConfig = { method: 'PUT', // 构建 URL,规避多/或少/问题 - url: this.buildURL(wpApiUrl, '/wp-json', endpoint), + url: this.buildURL(apiUrl, '/wp-json', endpoint), headers: { Authorization: `Basic ${auth}`, }, @@ -276,7 +278,7 @@ export class WPService { * @param data 更新的数据 */ async updateProduct( - site: WpSite, + site: any, productId: string, data: UpdateWpProductDTO ): Promise { @@ -295,7 +297,7 @@ export class WPService { * @param stock_status 上下架状态 */ async updateProductStatus( - site: WpSite, + site: any, productId: string, status: ProductStatus, stock_status: ProductStockStatus @@ -315,7 +317,7 @@ export class WPService { * @param data 更新的数据 */ async updateVariation( - site: WpSite, + site: any, productId: string, variationId: string, data: UpdateVariationDTO @@ -336,7 +338,7 @@ export class WPService { * 更新 Order */ async updateOrder( - site: WpSite, + site: any, orderId: string, data: Record ): Promise { @@ -344,11 +346,12 @@ export class WPService { } async createShipment( - site: WpSite, + site: any, orderId: string, data: Record ) { - const { wpApiUrl, consumerKey, consumerSecret } = site; + const apiUrl = site.apiUrl; + const { consumerKey, consumerSecret } = site; const auth = Buffer.from(`${consumerKey}:${consumerSecret}`).toString( 'base64' ); @@ -356,7 +359,7 @@ export class WPService { method: 'POST', // 构建 URL,规避多/或少/问题 url: this.buildURL( - wpApiUrl, + apiUrl, '/wp-json', 'wc-ast/v3/orders', orderId, @@ -371,11 +374,12 @@ export class WPService { } async deleteShipment( - site: WpSite, + site: any, orderId: string, trackingId: string, ): Promise { - const { wpApiUrl, consumerKey, consumerSecret } = site; + const apiUrl = site.apiUrl; + const { consumerKey, consumerSecret } = site; const auth = Buffer.from(`${consumerKey}:${consumerSecret}`).toString( 'base64' ); @@ -386,7 +390,7 @@ export class WPService { method: 'DELETE', // 构建 URL,规避多/或少/问题 url: this.buildURL( - wpApiUrl, + apiUrl, '/wp-json', 'wc-ast/v3/orders', orderId, diff --git a/src/service/wp_product.service.ts b/src/service/wp_product.service.ts index 3f570e6..187b758 100644 --- a/src/service/wp_product.service.ts +++ b/src/service/wp_product.service.ts @@ -1,7 +1,6 @@ import { Product } from '../entity/product.entity'; -import { Config, Inject, Provide } from '@midwayjs/core'; +import { Inject, Provide } from '@midwayjs/core'; import { WPService } from './wp.service'; -import { WpSite } from '../interface'; import { WpProduct } from '../entity/wp_product.entity'; import { InjectEntityModel } from '@midwayjs/typeorm'; import { And, Like, Not, Repository } from 'typeorm'; @@ -12,89 +11,91 @@ import { UpdateWpProductDTO, } from '../dto/wp_product.dto'; import { ProductStatus, ProductStockStatus } from '../enums/base.enum'; +import { SiteService } from './site.service'; @Provide() export class WpProductService { - @Config('wpSite') - sites: WpSite[]; + // 移除配置中的站点数组,统一从数据库获取站点信息 @Inject() private readonly wpApiService: WPService; + @Inject() + private readonly siteService: SiteService; + @InjectEntityModel(WpProduct) wpProductModel: Repository; @InjectEntityModel(Variation) variationModel: Repository; - getSite(id: string): WpSite { - let idx = this.sites.findIndex(item => item.id === id); - return this.sites[idx]; - } async syncAllSites() { - for (const site of this.sites) { + // 从数据库获取所有启用的站点,并逐站点同步产品与变体 + 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); + await this.syncProductAndVariations(String(site.id), product, variations); } } } - + // 同步一个网站 async syncSite(siteId: string) { - const site = this.getSite(siteId); + // 通过数据库获取站点并转换为 WpSite,用于后续 WooCommerce 同步 + const site = await this.siteService.get(Number(siteId), true); const externalProductIds = this.wpProductModel.createQueryBuilder('wp_product') - .select([ - 'wp_product.id ', + .select([ + 'wp_product.id ', 'wp_product.externalProductId ', ]) - .where('wp_product.siteId = :siteIds ', { + .where('wp_product.siteId = :siteIds ', { siteIds: siteId, }) - const rawResult = await externalProductIds.getRawMany(); + const rawResult = await externalProductIds.getRawMany(); - const externalIds = rawResult.map(item => item.externalProductId); + const externalIds = rawResult.map(item => item.externalProductId); -const excludeValues = []; + const excludeValues = []; const products = await this.wpApiService.getProducts(site); for (const product of products) { - excludeValues.push(String(product.id)); - const variations = + excludeValues.push(String(product.id)); + const variations = product.type === 'variable' ? await this.wpApiService.getVariations(site, product.id) : []; - - await this.syncProductAndVariations(site.id, product, variations); + + await this.syncProductAndVariations(String(site.id), product, variations); } const filteredIds = externalIds.filter(id => !excludeValues.includes(id)); - if(filteredIds.length!=0){ - await this.variationModel.createQueryBuilder('variation') - .update() - .set({ on_delete: true }) - .where(" variation.externalProductId in (:...filteredId) ",{filteredId:filteredIds}) - .execute(); + if (filteredIds.length != 0) { + await this.variationModel.createQueryBuilder('variation') + .update() + .set({ on_delete: true }) + .where(" variation.externalProductId in (:...filteredId) ", { filteredId: filteredIds }) + .execute(); - this.wpProductModel.createQueryBuilder('wp_product') - .update() - .set({ on_delete: true }) - .where(" wp_product.externalProductId in (:...filteredId) ",{filteredId:filteredIds}) - .execute(); -} + this.wpProductModel.createQueryBuilder('wp_product') + .update() + .set({ on_delete: true }) + .where(" wp_product.externalProductId in (:...filteredId) ", { filteredId: filteredIds }) + .execute(); + } } // 控制产品上下架 async updateProductStatus(id: number, status: ProductStatus, stock_status: ProductStockStatus) { const wpProduct = await this.wpProductModel.findOneBy({ id }); - const site = await this.getSite(wpProduct.siteId); + const site = await this.siteService.get(Number(wpProduct.siteId), true); wpProduct.status = status; wpProduct.stockStatus = stock_status; - const res = await this.wpApiService.updateProductStatus(site, wpProduct.externalProductId, status, stock_status); + const res = await this.wpApiService.updateProductStatus(site, wpProduct.externalProductId, status, stock_status); if (res === true) { this.wpProductModel.save(wpProduct); return true; @@ -310,8 +311,8 @@ const excludeValues = []; if (status) { where.status = status; } - where.on_delete = false; - + where.on_delete = false; + const products = await this.wpProductModel.find({ where, skip: (current - 1) * pageSize, @@ -492,16 +493,16 @@ const excludeValues = []; if (!product) throw new Error('未找到该商品'); await this.variationModel.createQueryBuilder('variation') - .update() - .set({ on_delete: true }) - .where(" variation.externalProductId = :externalProductId ",{externalProductId:productId}) - .execute(); + .update() + .set({ on_delete: true }) + .where(" variation.externalProductId = :externalProductId ", { externalProductId: productId }) + .execute(); - const sums= await this.wpProductModel.createQueryBuilder('wp_product') - .update() - .set({ on_delete: true }) - .where(" wp_product.externalProductId = :externalProductId ",{externalProductId:productId}) - .execute(); + const sums = await this.wpProductModel.createQueryBuilder('wp_product') + .update() + .set({ on_delete: true }) + .where(" wp_product.externalProductId = :externalProductId ", { externalProductId: productId }) + .execute(); console.log(sums); //await this.variationModel.delete({ siteId, externalProductId: productId }); @@ -509,7 +510,7 @@ const excludeValues = []; } - + async findProductsByName(name: string): Promise { const nameFilter = name ? name.split(' ').filter(Boolean) : []; const query = this.wpProductModel.createQueryBuilder('product'); @@ -542,7 +543,7 @@ const excludeValues = []; } query.take(50); - + return await query.getMany(); } }