Compare commits

..

No commits in common. "a59d5a7b7548118ca7aef39a80b630cd37b29b0e" and "c7480ccc8a5a650c2d623c77fd913ddfb49f2f86" have entirely different histories.

5 changed files with 77 additions and 86 deletions

View File

@ -52,7 +52,7 @@ export class WebhookController {
// 中文注释:从数据库获取站点配置 // 中文注释:从数据库获取站点配置
const site = await this.siteService.get(Number(siteId), true); const site = await this.siteService.get(Number(siteId), true);
if (!site || !source.includes(site.apiUrl)) { if (!site || !source.includes(site.wpApiUrl)) {
console.log('domain not match'); console.log('domain not match');
return { return {
code: HttpStatus.BAD_REQUEST, code: HttpStatus.BAD_REQUEST,
@ -97,13 +97,13 @@ export class WebhookController {
? await this.wpApiService.getVariations(site, body.id) ? await this.wpApiService.getVariations(site, body.id)
: []; : [];
await this.wpProductService.syncProductAndVariations( await this.wpProductService.syncProductAndVariations(
String(site.id), site.id,
body, body,
variations variations
); );
break; break;
case 'product.deleted': case 'product.deleted':
await this.wpProductService.delWpProduct(String(site.id), body.id); await this.wpProductService.delWpProduct(site.id, body.id);
break; break;
case 'order.created': case 'order.created':
case 'order.updated': case 'order.updated':

View File

@ -1351,8 +1351,7 @@ export class OrderService {
return { return {
...order, ...order,
siteName: site?.siteName, siteName: site?.siteName,
// 中文注释Site 实体无邮箱字段,这里返回空字符串保持兼容 email: site?.email,
email: '',
items, items,
sales, sales,
refundItems, refundItems,

View File

@ -12,38 +12,32 @@ export class SiteService {
siteModel: Repository<Site>; siteModel: Repository<Site>;
async syncFromConfig(sites: WpSite[] = []) { async syncFromConfig(sites: WpSite[] = []) {
// 将配置中的 WpSite 同步到数据库 Site 表(用于一次性导入或初始化) for (const s of sites) {
for (const siteConfig of sites) { const exist = await this.siteModel.findOne({ where: { siteName: s.siteName } });
// 按站点名称查询是否已存在记录
const exist = await this.siteModel.findOne({ where: { siteName: siteConfig.siteName } });
// 将 WpSite 字段映射为 Site 实体字段
const payload: Partial<Site> = { const payload: Partial<Site> = {
siteName: siteConfig.siteName, siteName: s.siteName,
apiUrl: (siteConfig as any).wpApiUrl, apiUrl: (s as any).wpApiUrl,
consumerKey: (siteConfig as any).consumerKey, consumerKey: (s as any).consumerKey,
consumerSecret: (siteConfig as any).consumerSecret, consumerSecret: (s as any).consumerSecret,
type: 'woocommerce', type: 'woocommerce',
}; };
// 存在则更新,不存在则插入新记录
if (exist) await this.siteModel.update({ id: exist.id }, payload); if (exist) await this.siteModel.update({ id: exist.id }, payload);
else await this.siteModel.insert(payload as Site); else await this.siteModel.insert(payload as Site);
} }
} }
async create(data: Partial<Site>) { async create(data: Partial<Site>) {
// 创建新的站点记录
await this.siteModel.insert(data as Site); await this.siteModel.insert(data as Site);
return true; return true;
} }
async update(id: string | number, data: UpdateSiteDTO) { async update(id: string | number, data: UpdateSiteDTO) {
// 更新指定站点记录,将布尔 isDisabled 转换为数值 0/1
const payload: Partial<Site> = { const payload: Partial<Site> = {
...data, ...data,
isDisabled: isDisabled:
data.isDisabled === undefined // 未传入则不更新该字段 data.isDisabled === undefined
? undefined ? undefined
: data.isDisabled // true -> 1, false -> 0 : data.isDisabled
? 1 ? 1
: 0, : 0,
} as any; } as any;
@ -52,25 +46,19 @@ export class SiteService {
} }
async get(id: string | number, includeSecret = false) { async get(id: string | number, includeSecret = false) {
// 根据主键获取站点includeSecret 为 true 时返回密钥字段 const s = await this.siteModel.findOne({ where: { id: Number(id) } });
const site = await this.siteModel.findOne({ where: { id: Number(id) } }); if (!s) return null;
if (!site) return null; if (includeSecret) return s;
if (includeSecret) return site; const { consumerKey, consumerSecret, emailPswd, ...rest } = s as any;
// 默认不返回密钥,进行字段脱敏
const { consumerKey, consumerSecret, ...rest } = site;
return rest; return rest;
} }
async list(param: { current?: number; pageSize?: number; keyword?: string; isDisabled?: boolean; ids?: string }, includeSecret = false) { 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 { current = 1, pageSize = 10, keyword, isDisabled, ids } = (param || {}) as any;
const where: any = {}; const where: any = {};
// 按名称模糊查询
if (keyword) where.siteName = Like(`%${keyword}%`); if (keyword) where.siteName = Like(`%${keyword}%`);
// 按禁用状态过滤(布尔转数值)
if (typeof isDisabled === 'boolean') where.isDisabled = isDisabled ? 1 : 0; if (typeof isDisabled === 'boolean') where.isDisabled = isDisabled ? 1 : 0;
if (ids) { if (ids) {
// 解析逗号分隔的 ID 字符串为数字数组,并过滤非法值
const numIds = String(ids) const numIds = String(ids)
.split(',') .split(',')
.filter(Boolean) .filter(Boolean)
@ -78,18 +66,15 @@ export class SiteService {
.filter((v) => !Number.isNaN(v)); .filter((v) => !Number.isNaN(v));
if (numIds.length > 0) where.id = In(numIds); 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 [items, total] = await this.siteModel.findAndCount({ where, skip: (current - 1) * pageSize, take: pageSize });
// 根据 includeSecret 决定是否脱敏返回密钥字段 const data = includeSecret ? items : items.map((s: any) => {
const data = includeSecret ? items : items.map((item: any) => { const { consumerKey, consumerSecret, ...rest } = s;
const { consumerKey, consumerSecret, ...rest } = item;
return rest; return rest;
}); });
return { items: data, total, current, pageSize }; return { items: data, total, current, pageSize };
} }
async disable(id: string | number, disabled: boolean) { async disable(id: string | number, disabled: boolean) {
// 设置站点禁用状态true -> 1, false -> 0
await this.siteModel.update({ id: Number(id) }, { isDisabled: disabled ? 1 : 0 }); await this.siteModel.update({ id: Number(id) }, { isDisabled: disabled ? 1 : 0 });
return true; return true;
} }

View File

@ -36,8 +36,9 @@ export class WPService {
* @param namespace API wc/v3 wcs/v1 * @param namespace API wc/v3 wcs/v1
*/ */
private createApi(site: any, namespace: WooCommerceRestApiVersion = 'wc/v3') { private createApi(site: any, namespace: WooCommerceRestApiVersion = 'wc/v3') {
const url = site.wpApiUrl ?? site.apiUrl; // 中文注释:兼容数据库 Site 与 WpSite 结构
return new WooCommerceRestApi({ return new WooCommerceRestApi({
url: site.apiUrl, url,
consumerKey: site.consumerKey, consumerKey: site.consumerKey,
consumerSecret: site.consumerSecret, consumerSecret: site.consumerSecret,
version: namespace, version: namespace,
@ -51,19 +52,15 @@ export class WPService {
const page = params.page ?? 1; const page = params.page ?? 1;
const per_page = params.per_page ?? 100; const per_page = params.per_page ?? 100;
const res = await api.get(resource.replace(/^\/+/, ''), { ...params, page, per_page }); 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 data = res.data as T[];
const totalPages = Number(res.headers?.['x-wp-totalpages'] ?? 1); const totalPages = Number(res.headers?.['x-wp-totalpages'] ?? 1);
const total = Number(res.headers?.['x-wp-total']?? 1) return { items: data, totalPages, page, per_page };
return { items: data, total, totalPages, page, per_page };
} }
/** /**
* SDK * SDK
*/ */
private async sdkGetAll<T>(api: WooCommerceRestApi, resource: string, params: Record<string, any> = {}, maxPages: number = 50): Promise<T[]> { private async sdkGetAll<T>(api: any, resource: string, params: Record<string, any> = {}, maxPages: number = 50): Promise<T[]> {
const result: T[] = []; const result: T[] = [];
for (let page = 1; page <= maxPages; page++) { for (let page = 1; page <= maxPages; page++) {
const { items, totalPages } = await this.sdkGetPage<T>(api, resource, { ...params, page }); const { items, totalPages } = await this.sdkGetPage<T>(api, resource, { ...params, page });
@ -89,7 +86,7 @@ export class WPService {
param: Record<string, any> = {} param: Record<string, any> = {}
): Promise<T> { ): Promise<T> {
try { try {
const apiUrl = site.apiUrl; const apiUrl = site.wpApiUrl ?? site.apiUrl;
const { consumerKey, consumerSecret } = site; const { consumerKey, consumerSecret } = site;
// 构建 URL规避多/或少/问题 // 构建 URL规避多/或少/问题
const url = this.buildURL(apiUrl, '/wp-json', endpoint); const url = this.buildURL(apiUrl, '/wp-json', endpoint);
@ -250,7 +247,7 @@ export class WPService {
site: any, site: any,
data: Record<string, any> data: Record<string, any>
): Promise<Boolean> { ): Promise<Boolean> {
const apiUrl = site.apiUrl; const apiUrl = site.wpApiUrl ?? site.apiUrl;
const { consumerKey, consumerSecret } = site; const { consumerKey, consumerSecret } = site;
const auth = Buffer.from(`${consumerKey}:${consumerSecret}`).toString( const auth = Buffer.from(`${consumerKey}:${consumerSecret}`).toString(
'base64' 'base64'
@ -350,7 +347,7 @@ export class WPService {
orderId: string, orderId: string,
data: Record<string, any> data: Record<string, any>
) { ) {
const apiUrl = site.apiUrl; const apiUrl = site.wpApiUrl ?? site.apiUrl;
const { consumerKey, consumerSecret } = site; const { consumerKey, consumerSecret } = site;
const auth = Buffer.from(`${consumerKey}:${consumerSecret}`).toString( const auth = Buffer.from(`${consumerKey}:${consumerSecret}`).toString(
'base64' 'base64'
@ -378,7 +375,7 @@ export class WPService {
orderId: string, orderId: string,
trackingId: string, trackingId: string,
): Promise<Boolean> { ): Promise<Boolean> {
const apiUrl = site.apiUrl; const apiUrl = site.wpApiUrl ?? site.apiUrl;
const { consumerKey, consumerSecret } = site; const { consumerKey, consumerSecret } = site;
const auth = Buffer.from(`${consumerKey}:${consumerSecret}`).toString( const auth = Buffer.from(`${consumerKey}:${consumerSecret}`).toString(
'base64' 'base64'

View File

@ -1,6 +1,7 @@
import { Product } from '../entity/product.entity'; import { Product } from '../entity/product.entity';
import { Inject, Provide } from '@midwayjs/core'; import { Inject, Provide } from '@midwayjs/core';
import { WPService } from './wp.service'; import { WPService } from './wp.service';
import { WpSite } from '../interface';
import { WpProduct } from '../entity/wp_product.entity'; import { WpProduct } from '../entity/wp_product.entity';
import { InjectEntityModel } from '@midwayjs/typeorm'; import { InjectEntityModel } from '@midwayjs/typeorm';
import { And, Like, Not, Repository } from 'typeorm'; import { And, Like, Not, Repository } from 'typeorm';
@ -32,19 +33,28 @@ export class WpProductService {
async syncAllSites() { async syncAllSites() {
// 中文注释:从数据库获取所有启用的站点,并逐站点同步产品与变体 // 中文注释:从数据库获取所有启用的站点,并逐站点同步产品与变体
const { items: sites } = await this.siteService.list({ current: 1, pageSize: Infinity, isDisabled: false }, true); const { items } = await this.siteService.list({ current: 1, pageSize: 1000, isDisabled: false }, true);
for (const site of sites) { 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); const products = await this.wpApiService.getProducts(site);
for (const product of products) { for (const product of products) {
const variations = const variations =
product.type === 'variable' product.type === 'variable'
? await this.wpApiService.getVariations(site, product.id) ? await this.wpApiService.getVariations(site, product.id)
: []; : [];
await this.syncProductAndVariations(String(site.id), product, variations); await this.syncProductAndVariations(site.id, product, variations);
} }
} }
} }
// 同步一个网站
async syncSite(siteId: string) { async syncSite(siteId: string) {
// 中文注释:通过数据库获取站点并转换为 WpSite用于后续 WooCommerce 同步 // 中文注释:通过数据库获取站点并转换为 WpSite用于后续 WooCommerce 同步
const site = await this.siteService.get(Number(siteId), true); const site = await this.siteService.get(Number(siteId), true);
@ -60,7 +70,7 @@ export class WpProductService {
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); const products = await this.wpApiService.getProducts(site);
for (const product of products) { for (const product of products) {
@ -70,23 +80,23 @@ export class WpProductService {
? await this.wpApiService.getVariations(site, product.id) ? await this.wpApiService.getVariations(site, product.id)
: []; : [];
await this.syncProductAndVariations(String(site.id), product, variations); await this.syncProductAndVariations(site.id, product, variations);
} }
const filteredIds = externalIds.filter(id => !excludeValues.includes(id)); const filteredIds = externalIds.filter(id => !excludeValues.includes(id));
if (filteredIds.length != 0) { if(filteredIds.length!=0){
await this.variationModel.createQueryBuilder('variation') await this.variationModel.createQueryBuilder('variation')
.update() .update()
.set({ on_delete: true }) .set({ on_delete: true })
.where(" variation.externalProductId in (:...filteredId) ", { filteredId: filteredIds }) .where(" variation.externalProductId in (:...filteredId) ",{filteredId:filteredIds})
.execute(); .execute();
this.wpProductModel.createQueryBuilder('wp_product') this.wpProductModel.createQueryBuilder('wp_product')
.update() .update()
.set({ on_delete: true }) .set({ on_delete: true })
.where(" wp_product.externalProductId in (:...filteredId) ", { filteredId: filteredIds }) .where(" wp_product.externalProductId in (:...filteredId) ",{filteredId:filteredIds})
.execute(); .execute();
} }
} }
// 控制产品上下架 // 控制产品上下架
@ -495,13 +505,13 @@ export class WpProductService {
await this.variationModel.createQueryBuilder('variation') await this.variationModel.createQueryBuilder('variation')
.update() .update()
.set({ on_delete: true }) .set({ on_delete: true })
.where(" variation.externalProductId = :externalProductId ", { externalProductId: productId }) .where(" variation.externalProductId = :externalProductId ",{externalProductId:productId})
.execute(); .execute();
const sums = await this.wpProductModel.createQueryBuilder('wp_product') const sums= await this.wpProductModel.createQueryBuilder('wp_product')
.update() .update()
.set({ on_delete: true }) .set({ on_delete: true })
.where(" wp_product.externalProductId = :externalProductId ", { externalProductId: productId }) .where(" wp_product.externalProductId = :externalProductId ",{externalProductId:productId})
.execute(); .execute();
console.log(sums); console.log(sums);