From ec6a8c3154b14aa54890ee775b700de58bdeb601 Mon Sep 17 00:00:00 2001 From: tikkhun Date: Fri, 21 Nov 2025 17:36:21 +0800 Subject: [PATCH 1/7] =?UTF-8?q?docs(enums):=20=E6=B7=BB=E5=8A=A0=E9=9D=99?= =?UTF-8?q?=E6=80=81=E9=AA=8C=E8=AF=81=E7=A0=81=E7=9A=84=E6=B3=A8=E9=87=8A?= =?UTF-8?q?=E8=AF=B4=E6=98=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/enums/base.enum.ts | 1 + 1 file changed, 1 insertion(+) 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' } -- 2.40.1 From c75d62051695ca25e4a730ca83989a93778e404a Mon Sep 17 00:00:00 2001 From: tikkhun Date: Sat, 22 Nov 2025 10:30:30 +0800 Subject: [PATCH 2/7] =?UTF-8?q?feat(=E7=AB=99=E7=82=B9):=20=E5=AE=9E?= =?UTF-8?q?=E7=8E=B0=E7=AB=99=E7=82=B9=E7=AE=A1=E7=90=86=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 添加站点实体、服务层和控制器,支持站点的CRUD操作 同步配置中的站点信息到数据库 提供站点禁用/启用功能 --- src/config/config.default.ts | 2 + src/configuration.ts | 7 +++ src/controller/site.controller.ts | 80 ++++++++++++++++++++++++------ src/dto/site.dto.ts | 62 ++++++++++++++++++++--- src/entity/site.entity.ts | 28 +++++++++++ src/service/site.service.ts | 81 +++++++++++++++++++++++++++++++ 6 files changed, 239 insertions(+), 21 deletions(-) create mode 100644 src/entity/site.entity.ts create mode 100644 src/service/site.service.ts 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/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/service/site.service.ts b/src/service/site.service.ts new file mode 100644 index 0000000..8510865 --- /dev/null +++ b/src/service/site.service.ts @@ -0,0 +1,81 @@ +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[] = []) { + for (const s of sites) { + const exist = await this.siteModel.findOne({ where: { siteName: s.siteName } }); + const payload: Partial = { + siteName: s.siteName, + apiUrl: (s as any).wpApiUrl, + consumerKey: (s as any).consumerKey, + consumerSecret: (s 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) { + const payload: Partial = { + ...data, + isDisabled: + data.isDisabled === undefined + ? undefined + : data.isDisabled + ? 1 + : 0, + } as any; + await this.siteModel.update({ id: Number(id) }, payload); + return true; + } + + async get(id: string | number, includeSecret = false) { + const s = await this.siteModel.findOne({ where: { id: Number(id) } }); + if (!s) return null; + if (includeSecret) return s; + const { consumerKey, consumerSecret, emailPswd, ...rest } = s as any; + return rest; + } + + async list(param: { current?: number; pageSize?: number; keyword?: string; isDisabled?: boolean; ids?: string }, includeSecret = false) { + 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) { + const numIds = String(ids) + .split(',') + .filter(Boolean) + .map((i) => Number(i)) + .filter((v) => !Number.isNaN(v)); + if (numIds.length > 0) where.id = In(numIds); + } + const [items, total] = await this.siteModel.findAndCount({ where, skip: (current - 1) * pageSize, take: pageSize }); + const data = includeSecret ? items : items.map((s: any) => { + const { consumerKey, consumerSecret, ...rest } = s; + return rest; + }); + return { items: data, total, current, pageSize }; + } + + async disable(id: string | number, disabled: boolean) { + await this.siteModel.update({ id: Number(id) }, { isDisabled: disabled ? 1 : 0 }); + return true; + } +} \ No newline at end of file -- 2.40.1 From 1d62730ca0074970e58235e5d53456a883a6894b Mon Sep 17 00:00:00 2001 From: tikkhun Date: Sat, 22 Nov 2025 10:47:10 +0800 Subject: [PATCH 3/7] =?UTF-8?q?feat(db):=20=E6=B7=BB=E5=8A=A0=E6=95=B0?= =?UTF-8?q?=E6=8D=AE=E5=BA=93=E7=A7=8D=E5=AD=90=E6=96=87=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/db/seed/index.ts | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 src/db/seed/index.ts diff --git a/src/db/seed/index.ts b/src/db/seed/index.ts new file mode 100644 index 0000000..e69de29 -- 2.40.1 From c7480ccc8a5a650c2d623c77fd913ddfb49f2f86 Mon Sep 17 00:00:00 2001 From: tikkhun Date: Sat, 22 Nov 2025 11:41:49 +0800 Subject: [PATCH 4/7] =?UTF-8?q?refactor(=E7=AB=99=E7=82=B9=E7=AE=A1?= =?UTF-8?q?=E7=90=86):=20=E7=A7=BB=E9=99=A4=E9=85=8D=E7=BD=AE=E4=B8=AD?= =?UTF-8?q?=E7=9A=84=E7=AB=99=E7=82=B9=E6=95=B0=E7=BB=84=EF=BC=8C=E7=BB=9F?= =?UTF-8?q?=E4=B8=80=E9=80=9A=E8=BF=87=E6=95=B0=E6=8D=AE=E5=BA=93=E8=8E=B7?= =?UTF-8?q?=E5=8F=96=E7=AB=99=E7=82=B9=E4=BF=A1=E6=81=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 重构多个控制器和服务,将硬编码的站点配置替换为通过 SiteService 从数据库获取 使用批量查询优化站点名称映射,避免 N+1 查询问题 兼容新旧站点数据结构,确保平滑过渡 --- src/controller/webhook.controller.ts | 13 ++-- src/controller/wp_product.controller.ts | 21 +++---- src/service/logistics.service.ts | 24 ++++--- src/service/order.service.ts | 45 +++++++------- src/service/wp.service.ts | 83 +++++++++++++------------ src/service/wp_product.service.ts | 31 ++++++--- 6 files changed, 112 insertions(+), 105 deletions(-) diff --git a/src/controller/webhook.controller.ts b/src/controller/webhook.controller.ts index 76714c5..f73c61e 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,7 +49,8 @@ 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)) { console.log('domain not match'); diff --git a/src/controller/wp_product.controller.ts b/src/controller/wp_product.controller.ts index e99bbaa..d903dfc 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/service/logistics.service.ts b/src/service/logistics.service.ts index 9f69895..0748974 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..a5c906c 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({ @@ -1415,20 +1414,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 +1443,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 +1457,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/wp.service.ts b/src/service/wp.service.ts index ae91d7b..31bd356 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,12 @@ 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') { + const url = site.wpApiUrl ?? site.apiUrl; // 中文注释:兼容数据库 Site 与 WpSite 结构 return new WooCommerceRestApi({ - url: site.wpApiUrl, + url, consumerKey: site.consumerKey, consumerSecret: site.consumerSecret, - // SDK 的版本字段有联合类型限制,这里兼容插件命名空间(例如 wcs/v1) version: namespace, }); } @@ -78,20 +78,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.wpApiUrl ?? 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 +109,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 +154,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 +178,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 +195,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 +207,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 +217,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 +227,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 +237,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.wpApiUrl ?? 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 +275,7 @@ export class WPService { * @param data 更新的数据 */ async updateProduct( - site: WpSite, + site: any, productId: string, data: UpdateWpProductDTO ): Promise { @@ -295,7 +294,7 @@ export class WPService { * @param stock_status 上下架状态 */ async updateProductStatus( - site: WpSite, + site: any, productId: string, status: ProductStatus, stock_status: ProductStockStatus @@ -315,7 +314,7 @@ export class WPService { * @param data 更新的数据 */ async updateVariation( - site: WpSite, + site: any, productId: string, variationId: string, data: UpdateVariationDTO @@ -336,7 +335,7 @@ export class WPService { * 更新 Order */ async updateOrder( - site: WpSite, + site: any, orderId: string, data: Record ): Promise { @@ -344,11 +343,12 @@ export class WPService { } async createShipment( - site: WpSite, + site: any, orderId: string, data: Record ) { - const { wpApiUrl, consumerKey, consumerSecret } = site; + const apiUrl = site.wpApiUrl ?? site.apiUrl; + const { consumerKey, consumerSecret } = site; const auth = Buffer.from(`${consumerKey}:${consumerSecret}`).toString( 'base64' ); @@ -356,7 +356,7 @@ export class WPService { method: 'POST', // 构建 URL,规避多/或少/问题 url: this.buildURL( - wpApiUrl, + apiUrl, '/wp-json', 'wc-ast/v3/orders', orderId, @@ -371,11 +371,12 @@ export class WPService { } async deleteShipment( - site: WpSite, + site: any, orderId: string, trackingId: string, ): Promise { - const { wpApiUrl, consumerKey, consumerSecret } = site; + const apiUrl = site.wpApiUrl ?? site.apiUrl; + const { consumerKey, consumerSecret } = site; const auth = Buffer.from(`${consumerKey}:${consumerSecret}`).toString( 'base64' ); @@ -386,7 +387,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..7da4afd 100644 --- a/src/service/wp_product.service.ts +++ b/src/service/wp_product.service.ts @@ -1,5 +1,5 @@ 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'; @@ -12,28 +12,38 @@ 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 } = await this.siteService.list({ current: 1, pageSize: 1000, isDisabled: false }, true); + for (const s of items as any[]) { + const site: WpSite = { + id: String(s.id), + wpApiUrl: s.apiUrl, + consumerKey: s.consumerKey, + consumerSecret: s.consumerSecret, + siteName: s.siteName, + email: '', + emailPswd: '', + }; const products = await this.wpApiService.getProducts(site); for (const product of products) { const variations = @@ -46,7 +56,8 @@ export class WpProductService { } 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 ', @@ -91,7 +102,7 @@ const excludeValues = []; // 控制产品上下架 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); -- 2.40.1 From a64e61129434111ea4b6605af137ec6626e15940 Mon Sep 17 00:00:00 2001 From: tikkhun Date: Mon, 24 Nov 2025 09:24:55 +0800 Subject: [PATCH 5/7] =?UTF-8?q?refactor(service):=20=E7=BB=9F=E4=B8=80?= =?UTF-8?q?=E4=BD=BF=E7=94=A8site.apiUrl=E5=B9=B6=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E4=BA=A7=E5=93=81=E5=90=8C=E6=AD=A5=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 移除对wpApiUrl的兼容处理,统一使用apiUrl 修复产品同步查询的分页问题,使用Infinity获取全部数据 优化代码格式和缩进 --- src/service/site.service.ts | 2 +- src/service/wp.service.ts | 11 +++-- src/service/wp_product.service.ts | 74 +++++++++++++++---------------- 3 files changed, 43 insertions(+), 44 deletions(-) diff --git a/src/service/site.service.ts b/src/service/site.service.ts index 8510865..157ddf1 100644 --- a/src/service/site.service.ts +++ b/src/service/site.service.ts @@ -52,7 +52,7 @@ export class SiteService { const { consumerKey, consumerSecret, emailPswd, ...rest } = s as any; return rest; } - + async list(param: { current?: number; pageSize?: number; keyword?: string; isDisabled?: boolean; ids?: string }, includeSecret = false) { const { current = 1, pageSize = 10, keyword, isDisabled, ids } = (param || {}) as any; const where: any = {}; diff --git a/src/service/wp.service.ts b/src/service/wp.service.ts index 31bd356..110ff84 100644 --- a/src/service/wp.service.ts +++ b/src/service/wp.service.ts @@ -36,9 +36,8 @@ export class WPService { * @param namespace API 命名空间,默认 wc/v3;订阅推荐 wcs/v1 */ private createApi(site: any, namespace: WooCommerceRestApiVersion = 'wc/v3') { - const url = site.wpApiUrl ?? site.apiUrl; // 中文注释:兼容数据库 Site 与 WpSite 结构 return new WooCommerceRestApi({ - url, + url: site.apiUrl, consumerKey: site.consumerKey, consumerSecret: site.consumerSecret, version: namespace, @@ -86,7 +85,7 @@ export class WPService { param: Record = {} ): Promise { try { - const apiUrl = site.wpApiUrl ?? site.apiUrl; + const apiUrl = site.apiUrl; const { consumerKey, consumerSecret } = site; // 构建 URL,规避多/或少/问题 const url = this.buildURL(apiUrl, '/wp-json', endpoint); @@ -247,7 +246,7 @@ export class WPService { site: any, data: Record ): Promise { - const apiUrl = site.wpApiUrl ?? site.apiUrl; + const apiUrl = site.apiUrl; const { consumerKey, consumerSecret } = site; const auth = Buffer.from(`${consumerKey}:${consumerSecret}`).toString( 'base64' @@ -347,7 +346,7 @@ export class WPService { orderId: string, data: Record ) { - const apiUrl = site.wpApiUrl ?? site.apiUrl; + const apiUrl = site.apiUrl; const { consumerKey, consumerSecret } = site; const auth = Buffer.from(`${consumerKey}:${consumerSecret}`).toString( 'base64' @@ -375,7 +374,7 @@ export class WPService { orderId: string, trackingId: string, ): Promise { - const apiUrl = site.wpApiUrl ?? site.apiUrl; + const apiUrl = site.apiUrl; const { consumerKey, consumerSecret } = site; const auth = Buffer.from(`${consumerKey}:${consumerSecret}`).toString( 'base64' diff --git a/src/service/wp_product.service.ts b/src/service/wp_product.service.ts index 7da4afd..ddc214a 100644 --- a/src/service/wp_product.service.ts +++ b/src/service/wp_product.service.ts @@ -33,7 +33,7 @@ export class WpProductService { async syncAllSites() { // 中文注释:从数据库获取所有启用的站点,并逐站点同步产品与变体 - const { items } = await this.siteService.list({ current: 1, pageSize: 1000, isDisabled: false }, true); + const { items } = await this.siteService.list({ current: 1, pageSize: Infinity, isDisabled: false }, true); for (const s of items as any[]) { const site: WpSite = { id: String(s.id), @@ -54,49 +54,49 @@ export class WpProductService { } } } - + // 同步一个网站 async syncSite(siteId: string) { // 中文注释:通过数据库获取站点并转换为 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); } 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(); + } } // 控制产品上下架 @@ -105,7 +105,7 @@ const excludeValues = []; 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; @@ -321,8 +321,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, @@ -503,16 +503,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 }); @@ -520,7 +520,7 @@ const excludeValues = []; } - + async findProductsByName(name: string): Promise { const nameFilter = name ? name.split(' ').filter(Boolean) : []; const query = this.wpProductModel.createQueryBuilder('product'); @@ -553,7 +553,7 @@ const excludeValues = []; } query.take(50); - + return await query.getMany(); } } -- 2.40.1 From a59d5a7b7548118ca7aef39a80b630cd37b29b0e Mon Sep 17 00:00:00 2001 From: tikkhun Date: Mon, 24 Nov 2025 09:48:53 +0800 Subject: [PATCH 6/7] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E7=AB=99=E7=82=B9?= =?UTF-8?q?=E7=9B=B8=E5=85=B3=E6=8E=A5=E5=8F=A3=E7=9A=84=E7=B1=BB=E5=9E=8B?= =?UTF-8?q?=E8=BD=AC=E6=8D=A2=E5=92=8C=E5=AD=97=E6=AE=B5=E5=A4=84=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 修复 order.service 中 email 字段返回空字符串以保持兼容 统一 webhook.controller 和 wp_product.service 中的 site.id 类型为字符串 在 wp.service 中添加接口返回内容类型检查并返回 total 字段 优化 site.service 的代码注释和字段处理逻辑 --- src/controller/webhook.controller.ts | 6 ++-- src/service/order.service.ts | 3 +- src/service/site.service.ts | 43 +++++++++++++++++++--------- src/service/wp.service.ts | 8 ++++-- src/service/wp_product.service.ts | 18 +++--------- 5 files changed, 44 insertions(+), 34 deletions(-) diff --git a/src/controller/webhook.controller.ts b/src/controller/webhook.controller.ts index f73c61e..02bdb38 100644 --- a/src/controller/webhook.controller.ts +++ b/src/controller/webhook.controller.ts @@ -52,7 +52,7 @@ export class WebhookController { // 中文注释:从数据库获取站点配置 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, @@ -97,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/service/order.service.ts b/src/service/order.service.ts index a5c906c..91848c6 100644 --- a/src/service/order.service.ts +++ b/src/service/order.service.ts @@ -1351,7 +1351,8 @@ export class OrderService { return { ...order, siteName: site?.siteName, - email: site?.email, + // 中文注释:Site 实体无邮箱字段,这里返回空字符串保持兼容 + email: '', items, sales, refundItems, diff --git a/src/service/site.service.ts b/src/service/site.service.ts index 157ddf1..c5f176b 100644 --- a/src/service/site.service.ts +++ b/src/service/site.service.ts @@ -12,32 +12,38 @@ export class SiteService { siteModel: Repository; async syncFromConfig(sites: WpSite[] = []) { - for (const s of sites) { - const exist = await this.siteModel.findOne({ where: { siteName: s.siteName } }); + // 将配置中的 WpSite 同步到数据库 Site 表(用于一次性导入或初始化) + for (const siteConfig of sites) { + // 按站点名称查询是否已存在记录 + const exist = await this.siteModel.findOne({ where: { siteName: siteConfig.siteName } }); + // 将 WpSite 字段映射为 Site 实体字段 const payload: Partial = { - siteName: s.siteName, - apiUrl: (s as any).wpApiUrl, - consumerKey: (s as any).consumerKey, - consumerSecret: (s as any).consumerSecret, + 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 + data.isDisabled === undefined // 未传入则不更新该字段 ? undefined - : data.isDisabled + : data.isDisabled // true -> 1, false -> 0 ? 1 : 0, } as any; @@ -46,19 +52,25 @@ export class SiteService { } async get(id: string | number, includeSecret = false) { - const s = await this.siteModel.findOne({ where: { id: Number(id) } }); - if (!s) return null; - if (includeSecret) return s; - const { consumerKey, consumerSecret, emailPswd, ...rest } = s as any; + // 根据主键获取站点;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) @@ -66,15 +78,18 @@ export class SiteService { .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 }); - const data = includeSecret ? items : items.map((s: any) => { - const { consumerKey, consumerSecret, ...rest } = s; + // 根据 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; } diff --git a/src/service/wp.service.ts b/src/service/wp.service.ts index 110ff84..443018f 100644 --- a/src/service/wp.service.ts +++ b/src/service/wp.service.ts @@ -51,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 }); diff --git a/src/service/wp_product.service.ts b/src/service/wp_product.service.ts index ddc214a..0e641e2 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 { 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'; @@ -33,24 +32,15 @@ export class WpProductService { async syncAllSites() { // 中文注释:从数据库获取所有启用的站点,并逐站点同步产品与变体 - const { items } = await this.siteService.list({ current: 1, pageSize: Infinity, isDisabled: false }, true); - for (const s of items as any[]) { - const site: WpSite = { - id: String(s.id), - wpApiUrl: s.apiUrl, - consumerKey: s.consumerKey, - consumerSecret: s.consumerSecret, - siteName: s.siteName, - email: '', - emailPswd: '', - }; + 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); } } } @@ -80,7 +70,7 @@ export class WpProductService { ? 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)); -- 2.40.1 From 927857a795cc1c2e541b55ab8babac1d5ddf255b Mon Sep 17 00:00:00 2001 From: tikkhun Date: Mon, 24 Nov 2025 10:08:49 +0800 Subject: [PATCH 7/7] =?UTF-8?q?refactor:=20=E7=BB=9F=E4=B8=80=E4=BB=8E?= =?UTF-8?q?=E6=95=B0=E6=8D=AE=E5=BA=93=E8=8E=B7=E5=8F=96=E7=AB=99=E7=82=B9?= =?UTF-8?q?=E4=BF=A1=E6=81=AF=E6=9B=BF=E4=BB=A3=E7=A1=AC=E7=BC=96=E7=A0=81?= =?UTF-8?q?=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/controller/webhook.controller.ts | 4 ++-- src/controller/wp_product.controller.ts | 2 +- src/service/logistics.service.ts | 2 +- src/service/order.service.ts | 6 +++--- src/service/wp_product.service.ts | 6 +++--- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/controller/webhook.controller.ts b/src/controller/webhook.controller.ts index 02bdb38..d15da14 100644 --- a/src/controller/webhook.controller.ts +++ b/src/controller/webhook.controller.ts @@ -33,7 +33,7 @@ export class WebhookController { @Inject() private readonly siteService: SiteService; - // 中文注释:移除配置中的站点数组,来源统一改为数据库 + // 移除配置中的站点数组,来源统一改为数据库 @Get('/') async test() { @@ -49,7 +49,7 @@ export class WebhookController { const signature = header['x-wc-webhook-signature']; const topic = header['x-wc-webhook-topic']; const source = header['x-wc-webhook-source']; - // 中文注释:从数据库获取站点配置 + // 从数据库获取站点配置 const site = await this.siteService.get(Number(siteId), true); if (!site || !source.includes(site.apiUrl)) { diff --git a/src/controller/wp_product.controller.ts b/src/controller/wp_product.controller.ts index d903dfc..8623b1d 100644 --- a/src/controller/wp_product.controller.ts +++ b/src/controller/wp_product.controller.ts @@ -25,7 +25,7 @@ import { } from '../dto/reponse.dto'; @Controller('/wp_product') export class WpProductController { - // 中文注释:移除控制器内的配置站点引用,统一由服务层处理站点数据 + // 移除控制器内的配置站点引用,统一由服务层处理站点数据 @Inject() private readonly wpProductService: WpProductService; diff --git a/src/service/logistics.service.ts b/src/service/logistics.service.ts index 0748974..a79503d 100644 --- a/src/service/logistics.service.ts +++ b/src/service/logistics.service.ts @@ -558,7 +558,7 @@ export class LogisticsService { }, }); - // 中文注释:从数据库批量获取站点信息,构建映射以避免 N+1 查询 + // 从数据库批量获取站点信息,构建映射以避免 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])); diff --git a/src/service/order.service.ts b/src/service/order.service.ts index 91848c6..d62baf9 100644 --- a/src/service/order.service.ts +++ b/src/service/order.service.ts @@ -130,7 +130,7 @@ export class OrderService { } try { const site = await this.siteService.get(siteId); - // 中文注释:将订单状态同步到 WooCommerce,然后切换至下一状态 + // 将订单状态同步到 WooCommerce,然后切换至下一状态 await this.wpService.updateOrder(site, String(order.id), { status: order.status }); order.status = this.orderAutoNextStatusMap[originStatus]; } catch (error) { @@ -1351,7 +1351,7 @@ export class OrderService { return { ...order, siteName: site?.siteName, - // 中文注释:Site 实体无邮箱字段,这里返回空字符串保持兼容 + // Site 实体无邮箱字段,这里返回空字符串保持兼容 email: '', items, sales, @@ -1415,7 +1415,7 @@ export class OrderService { ]), }, }); - // 中文注释:批量获取订单涉及的站点名称,避免使用配置文件 + // 批量获取订单涉及的站点名称,避免使用配置文件 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])); diff --git a/src/service/wp_product.service.ts b/src/service/wp_product.service.ts index 0e641e2..187b758 100644 --- a/src/service/wp_product.service.ts +++ b/src/service/wp_product.service.ts @@ -15,7 +15,7 @@ import { SiteService } from './site.service'; @Provide() export class WpProductService { - // 中文注释:移除配置中的站点数组,统一从数据库获取站点信息 + // 移除配置中的站点数组,统一从数据库获取站点信息 @Inject() private readonly wpApiService: WPService; @@ -31,7 +31,7 @@ export class WpProductService { 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); @@ -46,7 +46,7 @@ export class WpProductService { } // 同步一个网站 async syncSite(siteId: string) { - // 中文注释:通过数据库获取站点并转换为 WpSite,用于后续 WooCommerce 同步 + // 通过数据库获取站点并转换为 WpSite,用于后续 WooCommerce 同步 const site = await this.siteService.get(Number(siteId), true); const externalProductIds = this.wpProductModel.createQueryBuilder('wp_product') .select([ -- 2.40.1