chore: 兼容旧数据库 #36
|
|
@ -16,6 +16,6 @@ yarn.lock
|
|||
**/config.local.ts
|
||||
container
|
||||
scripts
|
||||
ai
|
||||
tmp_uploads/
|
||||
.trae
|
||||
docs
|
||||
|
|
@ -321,6 +321,11 @@ export class ShopyyAdapter implements ISiteAdapter {
|
|||
};
|
||||
}
|
||||
|
||||
async getAllProducts(params?: UnifiedSearchParamsDTO): Promise<UnifiedProductDTO[]> {
|
||||
// Shopyy getAllProducts 暂未实现
|
||||
throw new Error('Shopyy getAllProducts 暂未实现');
|
||||
}
|
||||
|
||||
async getProduct(id: string | number): Promise<UnifiedProductDTO> {
|
||||
// 使用ShopyyService获取单个产品
|
||||
const product = await this.shopyyService.getProduct(this.site, id);
|
||||
|
|
@ -385,6 +390,11 @@ export class ShopyyAdapter implements ISiteAdapter {
|
|||
};
|
||||
}
|
||||
|
||||
async getAllOrders(params?: UnifiedSearchParamsDTO): Promise<UnifiedOrderDTO[]> {
|
||||
// Shopyy getAllOrders 暂未实现
|
||||
throw new Error('Shopyy getAllOrders 暂未实现');
|
||||
}
|
||||
|
||||
async getOrder(id: string | number): Promise<UnifiedOrderDTO> {
|
||||
const data = await this.shopyyService.getOrder(String(this.site.id), String(id));
|
||||
return this.mapOrder(data);
|
||||
|
|
@ -478,6 +488,11 @@ export class ShopyyAdapter implements ISiteAdapter {
|
|||
throw new Error('Shopyy does not support subscriptions.');
|
||||
}
|
||||
|
||||
async getAllSubscriptions(params?: UnifiedSearchParamsDTO): Promise<UnifiedSubscriptionDTO[]> {
|
||||
// Shopyy getAllSubscriptions 暂未实现
|
||||
throw new Error('Shopyy getAllSubscriptions 暂未实现');
|
||||
}
|
||||
|
||||
async getMedia(
|
||||
params: UnifiedSearchParamsDTO
|
||||
): Promise<UnifiedPaginationDTO<UnifiedMediaDTO>> {
|
||||
|
|
@ -488,7 +503,7 @@ export class ShopyyAdapter implements ISiteAdapter {
|
|||
requestParams
|
||||
);
|
||||
return {
|
||||
items: items.map(this.mapMedia),
|
||||
items: items.map(this.mapMedia.bind(this)),
|
||||
total,
|
||||
totalPages,
|
||||
page,
|
||||
|
|
@ -496,6 +511,11 @@ export class ShopyyAdapter implements ISiteAdapter {
|
|||
};
|
||||
}
|
||||
|
||||
async getAllMedia(params?: UnifiedSearchParamsDTO): Promise<UnifiedMediaDTO[]> {
|
||||
// Shopyy getAllMedia 暂未实现
|
||||
throw new Error('Shopyy getAllMedia 暂未实现');
|
||||
}
|
||||
|
||||
async createMedia(file: any): Promise<UnifiedMediaDTO> {
|
||||
const createdMedia = await this.shopyyService.createMedia(this.site, file);
|
||||
return this.mapMedia(createdMedia);
|
||||
|
|
@ -527,6 +547,11 @@ export class ShopyyAdapter implements ISiteAdapter {
|
|||
};
|
||||
}
|
||||
|
||||
async getAllReviews(params?: UnifiedSearchParamsDTO): Promise<UnifiedReviewDTO[]> {
|
||||
// Shopyy getAllReviews 暂未实现
|
||||
throw new Error('Shopyy getAllReviews 暂未实现');
|
||||
}
|
||||
|
||||
async getReview(id: string | number): Promise<UnifiedReviewDTO> {
|
||||
const review = await this.shopyyService.getReview(this.site, id);
|
||||
return this.mapReview(review);
|
||||
|
|
@ -550,7 +575,7 @@ export class ShopyyAdapter implements ISiteAdapter {
|
|||
}
|
||||
|
||||
private mapReviewSearchParams(params: UnifiedSearchParamsDTO): any {
|
||||
const { search, page, per_page, status } = params;
|
||||
const { search, page, per_page, where } = params;
|
||||
const shopyyParams: any = {
|
||||
page: page || 1,
|
||||
limit: per_page || 10,
|
||||
|
|
@ -560,8 +585,8 @@ export class ShopyyAdapter implements ISiteAdapter {
|
|||
shopyyParams.search = search;
|
||||
}
|
||||
|
||||
if (status) {
|
||||
shopyyParams.status = status;
|
||||
if (where.status) {
|
||||
shopyyParams.status = where.status;
|
||||
}
|
||||
|
||||
// if (product_id) {
|
||||
|
|
@ -607,6 +632,11 @@ export class ShopyyAdapter implements ISiteAdapter {
|
|||
};
|
||||
}
|
||||
|
||||
async getAllWebhooks(params?: UnifiedSearchParamsDTO): Promise<UnifiedWebhookDTO[]> {
|
||||
// Shopyy getAllWebhooks 暂未实现
|
||||
throw new Error('Shopyy getAllWebhooks 暂未实现');
|
||||
}
|
||||
|
||||
async getWebhook(id: string | number): Promise<UnifiedWebhookDTO> {
|
||||
const webhook = await this.shopyyService.getWebhook(this.site, id);
|
||||
return this.mapWebhook(webhook);
|
||||
|
|
@ -657,6 +687,11 @@ export class ShopyyAdapter implements ISiteAdapter {
|
|||
};
|
||||
}
|
||||
|
||||
async getAllCustomers(params?: UnifiedSearchParamsDTO): Promise<UnifiedCustomerDTO[]> {
|
||||
// Shopyy getAllCustomers 暂未实现
|
||||
throw new Error('Shopyy getAllCustomers 暂未实现');
|
||||
}
|
||||
|
||||
async getCustomer(id: string | number): Promise<UnifiedCustomerDTO> {
|
||||
const customer = await this.shopyyService.getCustomer(this.site, id);
|
||||
return this.mapCustomer(customer);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import { ISiteAdapter } from '../interface/site-adapter.interface';
|
||||
import { IPlatformService } from '../interface/platform.interface';
|
||||
import {
|
||||
UnifiedMediaDTO,
|
||||
UnifiedOrderDTO,
|
||||
|
|
@ -26,10 +25,11 @@ import {
|
|||
WooProductSearchParams,
|
||||
} from '../dto/woocommerce.dto';
|
||||
import { Site } from '../entity/site.entity';
|
||||
import { WPService } from '../service/wp.service';
|
||||
|
||||
export class WooCommerceAdapter implements ISiteAdapter {
|
||||
// 构造函数接收站点配置与服务实例
|
||||
constructor(private site: Site, private wpService: IPlatformService) {
|
||||
constructor(private site: Site, private wpService: WPService) {
|
||||
this.mapProduct = this.mapProduct.bind(this);
|
||||
this.mapReview = this.mapReview.bind(this);
|
||||
this.mapCustomer = this.mapCustomer.bind(this);
|
||||
|
|
@ -71,6 +71,17 @@ export class WooCommerceAdapter implements ISiteAdapter {
|
|||
}
|
||||
}
|
||||
|
||||
// 获取所有webhooks
|
||||
async getAllWebhooks(params?: UnifiedSearchParamsDTO): Promise<UnifiedWebhookDTO[]> {
|
||||
try {
|
||||
const api = (this.wpService as any).createApi(this.site, 'wc/v3');
|
||||
const webhooks = await this.wpService.sdkGetAll(api, 'webhooks', params);
|
||||
return webhooks.map((webhook: any) => this.mapWebhook(webhook));
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to get all webhooks: ${error instanceof Error ? error.message : String(error)}`);
|
||||
}
|
||||
}
|
||||
|
||||
// 获取单个 webhook 详情
|
||||
async getWebhook(id: string | number): Promise<UnifiedWebhookDTO> {
|
||||
try {
|
||||
|
|
@ -159,26 +170,10 @@ export class WooCommerceAdapter implements ISiteAdapter {
|
|||
const page = Number(params.page ?? 1);
|
||||
const per_page = Number( params.per_page ?? 20);
|
||||
const where = params.where && typeof params.where === 'object' ? params.where : {};
|
||||
let orderby: string | undefined = params.orderby;
|
||||
let order: 'asc' | 'desc' | undefined = params.orderDir as any;
|
||||
if (!orderby && params.order && typeof params.order === 'object') {
|
||||
const entries = Object.entries(params.order as Record<string, any>);
|
||||
if (entries.length > 0) {
|
||||
const [field, dir] = entries[0];
|
||||
let mappedField = field;
|
||||
if (['created_at', 'date_created', 'date'].includes(field)) mappedField = 'date';
|
||||
else if (['name', 'title'].includes(field)) mappedField = 'title';
|
||||
else if (['id', 'ID'].includes(field)) mappedField = 'id';
|
||||
else if (['price', 'regular_price', 'sale_price'].includes(field)) mappedField = 'price';
|
||||
orderby = mappedField;
|
||||
order = String(dir).toLowerCase() === 'desc' ? 'desc' : 'asc';
|
||||
}
|
||||
}
|
||||
|
||||
const mapped: any = {
|
||||
...(params.search ? { search: params.search } : {}),
|
||||
...(params.status ? { status: params.status } : {}),
|
||||
...(orderby ? { orderby } : {}),
|
||||
...(order ? { order } : {}),
|
||||
...(where.status ? { status: where.status } : {}),
|
||||
page,
|
||||
per_page,
|
||||
};
|
||||
|
|
@ -224,10 +219,6 @@ export class WooCommerceAdapter implements ISiteAdapter {
|
|||
if (where.virtual !== undefined) mapped.virtual = Boolean(where.virtual);
|
||||
if (where.downloadable !== undefined) mapped.downloadable = Boolean(where.downloadable);
|
||||
|
||||
if (params.ids) {
|
||||
const idsArr = String(params.ids).split(',').map(v => v.trim()).filter(Boolean);
|
||||
mapped.include = idsArr;
|
||||
}
|
||||
return mapped;
|
||||
}
|
||||
|
||||
|
|
@ -237,30 +228,12 @@ export class WooCommerceAdapter implements ISiteAdapter {
|
|||
const per_page = Number( params.per_page ?? 20);
|
||||
// 解析排序参数 支持从 order 对象推导
|
||||
const where = params.where && typeof params.where === 'object' ? params.where : {};
|
||||
let orderby: string | undefined = params.orderby;
|
||||
let orderDir: 'asc' | 'desc' | undefined = params.orderDir as any;
|
||||
if (!orderby && params.order && typeof params.order === 'object') {
|
||||
const entries = Object.entries(params.order as Record<string, any>);
|
||||
if (entries.length > 0) {
|
||||
const [field, dir] = entries[0];
|
||||
let mappedField = field;
|
||||
if (['created_at', 'date_created', 'date'].includes(field)) mappedField = 'date';
|
||||
else if (['modified', 'date_modified'].includes(field)) mappedField = 'modified';
|
||||
else if (['id', 'ID'].includes(field)) mappedField = 'id';
|
||||
else if (['name', 'title'].includes(field)) mappedField = 'title';
|
||||
else if (['slug'].includes(field)) mappedField = 'slug';
|
||||
else if (['include'].includes(field)) mappedField = 'include';
|
||||
orderby = mappedField;
|
||||
orderDir = String(dir).toLowerCase() === 'asc' ? 'asc' : 'desc';
|
||||
}
|
||||
} else if (!orderDir && typeof params.order === 'string') {
|
||||
orderDir = String(params.order).toLowerCase() === 'asc' ? 'asc' : 'desc';
|
||||
}
|
||||
|
||||
// if (params.orderBy && typeof params.orderBy === 'object') {
|
||||
// }
|
||||
const mapped: any = {
|
||||
...(params.search ? { search: params.search } : {}),
|
||||
...(orderby ? { orderby } : {}),
|
||||
...(orderDir ? { order: orderDir } : {}),
|
||||
// ...(orderBy ? { orderBy } : {}),
|
||||
|
|
||||
page,
|
||||
per_page,
|
||||
};
|
||||
|
|
@ -287,13 +260,13 @@ export class WooCommerceAdapter implements ISiteAdapter {
|
|||
// 集合过滤参数
|
||||
if (where.exclude) mapped.exclude = toArray(where.exclude);
|
||||
if (where.include) mapped.include = toArray(where.include);
|
||||
if (params.ids) mapped.include = String(params.ids).split(',').map(v => v.trim()).filter(Boolean);
|
||||
if (where.ids) mapped.include = toArray(where.ids);
|
||||
if (toNumber(where.offset) !== undefined) mapped.offset = Number(where.offset);
|
||||
if (where.parent ?? where.parentId) mapped.parent = toArray(where.parent ?? where.parentId);
|
||||
if (where.parent_exclude ?? where.parentExclude) mapped.parent_exclude = toArray(where.parent_exclude ?? where.parentExclude);
|
||||
|
||||
// 状态过滤 参数支持数组或逗号分隔字符串
|
||||
const statusSource = params.status ?? where.status;
|
||||
const statusSource = where.status;
|
||||
if (statusSource !== undefined) {
|
||||
mapped.status = Array.isArray(statusSource)
|
||||
? statusSource.map(s => String(s))
|
||||
|
|
@ -301,7 +274,7 @@ export class WooCommerceAdapter implements ISiteAdapter {
|
|||
}
|
||||
|
||||
// 客户与产品过滤
|
||||
const customerVal = params.customer_id ?? where.customer ?? where.customer_id;
|
||||
const customerVal = where.customer ?? where.customer_id;
|
||||
const productVal = where.product ?? where.product_id;
|
||||
const dpVal = where.dp;
|
||||
if (toNumber(customerVal) !== undefined) mapped.customer = Number(customerVal);
|
||||
|
|
@ -321,28 +294,10 @@ export class WooCommerceAdapter implements ISiteAdapter {
|
|||
const page = Number(params.page ?? 1);
|
||||
const per_page = Number(params.per_page ?? 20);
|
||||
const where = params.where && typeof params.where === 'object' ? params.where : {};
|
||||
let orderby: string | undefined = params.orderby;
|
||||
let orderDir: 'asc' | 'desc' | undefined = params.orderDir as any;
|
||||
if (!orderby && params.order && typeof params.order === 'object') {
|
||||
const entries = Object.entries(params.order as Record<string, any>);
|
||||
if (entries.length > 0) {
|
||||
const [field, dir] = entries[0];
|
||||
let mappedField = field;
|
||||
if (['id', 'ID'].includes(field)) mappedField = 'id';
|
||||
else if (['include'].includes(field)) mappedField = 'include';
|
||||
else if (['name', 'username'].includes(field)) mappedField = 'name';
|
||||
else if (['registered_date', 'date_created', 'registered', 'registeredDate'].includes(field)) mappedField = 'registered_date';
|
||||
orderby = mappedField;
|
||||
orderDir = String(dir).toLowerCase() === 'asc' ? 'asc' : 'desc';
|
||||
}
|
||||
} else if (!orderDir && typeof params.order === 'string') {
|
||||
orderDir = String(params.order).toLowerCase() === 'asc' ? 'asc' : 'desc';
|
||||
}
|
||||
|
||||
|
||||
const mapped: any = {
|
||||
...(params.search ? { search: params.search } : {}),
|
||||
...(orderby ? { orderby } : {}),
|
||||
...(orderDir ? { order: orderDir } : {}),
|
||||
page,
|
||||
per_page,
|
||||
};
|
||||
|
|
@ -361,11 +316,11 @@ export class WooCommerceAdapter implements ISiteAdapter {
|
|||
|
||||
if (where.exclude) mapped.exclude = toArray(where.exclude);
|
||||
if (where.include) mapped.include = toArray(where.include);
|
||||
if (params.ids) mapped.include = String(params.ids).split(',').map(v => v.trim()).filter(Boolean);
|
||||
if (where.ids) mapped.include = toArray(where.ids);
|
||||
if (toNumber(where.offset) !== undefined) mapped.offset = Number(where.offset);
|
||||
|
||||
if (where.email) mapped.email = String(where.email);
|
||||
const roleSource = where.role ?? params.status;
|
||||
const roleSource = where.role;
|
||||
if (roleSource !== undefined) mapped.role = String(roleSource);
|
||||
|
||||
return mapped;
|
||||
|
|
@ -522,6 +477,13 @@ export class WooCommerceAdapter implements ISiteAdapter {
|
|||
};
|
||||
}
|
||||
|
||||
async getAllProducts(params?: UnifiedSearchParamsDTO): Promise<UnifiedProductDTO[]> {
|
||||
// 使用sdkGetAll获取所有产品数据,不受分页限制
|
||||
const api = (this.wpService as any).createApi(this.site, 'wc/v3');
|
||||
const products = await this.wpService.sdkGetAll(api, 'products', params);
|
||||
return products.map((product: any) => this.mapProduct(product));
|
||||
}
|
||||
|
||||
async getProduct(id: string | number): Promise<UnifiedProductDTO> {
|
||||
// 获取单个产品详情并映射为统一产品DTO
|
||||
const api = (this.wpService as any).createApi(this.site, 'wc/v3');
|
||||
|
|
@ -602,6 +564,13 @@ export class WooCommerceAdapter implements ISiteAdapter {
|
|||
return this.mapOrder(res.data);
|
||||
}
|
||||
|
||||
async getAllOrders(params?: UnifiedSearchParamsDTO): Promise<UnifiedOrderDTO[]> {
|
||||
// 使用sdkGetAll获取所有订单数据,不受分页限制
|
||||
const api = (this.wpService as any).createApi(this.site, 'wc/v3');
|
||||
const orders = await this.wpService.sdkGetAll(api, 'orders', params);
|
||||
return orders.map((order: any) => this.mapOrder(order));
|
||||
}
|
||||
|
||||
async createOrder(data: Partial<UnifiedOrderDTO>): Promise<UnifiedOrderDTO> {
|
||||
// 创建订单并返回统一订单DTO
|
||||
const api = (this.wpService as any).createApi(this.site, 'wc/v3');
|
||||
|
|
@ -705,6 +674,13 @@ export class WooCommerceAdapter implements ISiteAdapter {
|
|||
};
|
||||
}
|
||||
|
||||
async getAllSubscriptions(params?: UnifiedSearchParamsDTO): Promise<UnifiedSubscriptionDTO[]> {
|
||||
// 使用sdkGetAll获取所有订阅数据,不受分页限制
|
||||
const api = (this.wpService as any).createApi(this.site, 'wc/v3');
|
||||
const subscriptions = await this.wpService.sdkGetAll(api, 'subscriptions', params);
|
||||
return subscriptions.map((subscription: any) => this.mapSubscription(subscription));
|
||||
}
|
||||
|
||||
async getMedia(
|
||||
params: UnifiedSearchParamsDTO
|
||||
): Promise<UnifiedPaginationDTO<UnifiedMediaDTO>> {
|
||||
|
|
@ -722,6 +698,13 @@ export class WooCommerceAdapter implements ISiteAdapter {
|
|||
};
|
||||
}
|
||||
|
||||
async getAllMedia(params?: UnifiedSearchParamsDTO): Promise<UnifiedMediaDTO[]> {
|
||||
// 使用sdkGetAll获取所有媒体数据,不受分页限制
|
||||
const api = (this.wpService as any).createApi(this.site, 'wc/v3');
|
||||
const media = await this.wpService.sdkGetAll(api, 'media', params);
|
||||
return media.map((mediaItem: any) => this.mapMedia(mediaItem));
|
||||
}
|
||||
|
||||
private mapReview(item: any): UnifiedReviewDTO & {raw: any} {
|
||||
// 将 WooCommerce 评论数据映射为统一评论DTO
|
||||
return {
|
||||
|
|
@ -757,6 +740,13 @@ export class WooCommerceAdapter implements ISiteAdapter {
|
|||
};
|
||||
}
|
||||
|
||||
async getAllReviews(params?: UnifiedSearchParamsDTO): Promise<UnifiedReviewDTO[]> {
|
||||
// 使用sdkGetAll获取所有评论数据,不受分页限制
|
||||
const api = (this.wpService as any).createApi(this.site, 'wc/v3');
|
||||
const reviews = await this.wpService.sdkGetAll(api, 'products/reviews', params);
|
||||
return reviews.map((review: any) => this.mapReview(review));
|
||||
}
|
||||
|
||||
async createReview(data: any): Promise<UnifiedReviewDTO> {
|
||||
const res = await this.wpService.createReview(this.site, data);
|
||||
return this.mapReview(res);
|
||||
|
|
@ -808,7 +798,6 @@ export class WooCommerceAdapter implements ISiteAdapter {
|
|||
raw: item,
|
||||
};
|
||||
}
|
||||
|
||||
async getCustomers(params: UnifiedSearchParamsDTO): Promise<UnifiedPaginationDTO<UnifiedCustomerDTO>> {
|
||||
const requestParams = this.mapCustomerSearchParams(params);
|
||||
const { items, total, totalPages, page, per_page } = await this.wpService.fetchResourcePaged<any>(
|
||||
|
|
@ -826,6 +815,13 @@ export class WooCommerceAdapter implements ISiteAdapter {
|
|||
};
|
||||
}
|
||||
|
||||
async getAllCustomers(params?: UnifiedSearchParamsDTO): Promise<UnifiedCustomerDTO[]> {
|
||||
// 使用sdkGetAll获取所有客户数据,不受分页限制
|
||||
const api = (this.wpService as any).createApi(this.site, 'wc/v3');
|
||||
const customers = await this.wpService.sdkGetAll(api, 'customers', params);
|
||||
return customers.map((customer: any) => this.mapCustomer(customer));
|
||||
}
|
||||
|
||||
async getCustomer(id: string | number): Promise<UnifiedCustomerDTO> {
|
||||
const api = (this.wpService as any).createApi(this.site, 'wc/v3');
|
||||
const res = await api.get(`customers/${id}`);
|
||||
|
|
@ -850,3 +846,4 @@ export class WooCommerceAdapter implements ISiteAdapter {
|
|||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,6 @@
|
|||
import { MidwayConfig } from '@midwayjs/core';
|
||||
import { join } from 'path';
|
||||
import { Product } from '../entity/product.entity';
|
||||
import { WpProduct } from '../entity/wp_product.entity';
|
||||
import { Variation } from '../entity/variation.entity';
|
||||
import { User } from '../entity/user.entity';
|
||||
import { PurchaseOrder } from '../entity/purchase_order.entity';
|
||||
import { PurchaseOrderItem } from '../entity/purchase_order_item.entity';
|
||||
|
|
@ -53,8 +51,6 @@ export default {
|
|||
Product,
|
||||
ProductStockComponent,
|
||||
ProductSiteSku,
|
||||
WpProduct,
|
||||
Variation,
|
||||
User,
|
||||
PurchaseOrder,
|
||||
PurchaseOrderItem,
|
||||
|
|
@ -116,17 +112,6 @@ export default {
|
|||
// secret: 'YOONE2024!@abc',
|
||||
// expiresIn: '7d',
|
||||
// },
|
||||
// wpSite: [
|
||||
// {
|
||||
// id: '2',
|
||||
// wpApiUrl: 'http://localhost:10004',
|
||||
// consumerKey: 'ck_dc9e151e9048c8ed3e27f35ac79d2bf7d6840652',
|
||||
// consumerSecret: 'cs_d05d625d7b0ac05c6d765671d8417f41d9477e38',
|
||||
// name: 'Local',
|
||||
// email: 'tom@yoonevape.com',
|
||||
// emailPswd: '',
|
||||
// },
|
||||
// ],
|
||||
swagger: {
|
||||
auth: {
|
||||
name: 'authorization',
|
||||
|
|
|
|||
|
|
@ -16,8 +16,10 @@ export default {
|
|||
dataSource: {
|
||||
default: {
|
||||
host: 'localhost',
|
||||
port: "3306",
|
||||
username: 'root',
|
||||
password: '12345678',
|
||||
password: 'root',
|
||||
database: 'inventory',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
@ -25,46 +27,12 @@ export default {
|
|||
origin: '*', // 允许所有来源跨域请求
|
||||
allowMethods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'], // 允许的 HTTP 方法
|
||||
allowHeaders: ['Content-Type', 'Authorization'], // 允许的自定义请求头
|
||||
credentials: true, // 允许携带凭据(cookies等)
|
||||
credentials: true, // 允许携带凭据(cookies等)
|
||||
},
|
||||
jwt: {
|
||||
secret: 'YOONE2024!@abc',
|
||||
expiresIn: '7d',
|
||||
},
|
||||
wpSite: [
|
||||
{
|
||||
id: '-1',
|
||||
siteName: 'Admin',
|
||||
email: '2469687281@qq.com',
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
wpApiUrl: 'http://t2-shop.local/',
|
||||
consumerKey: 'ck_a369473a6451dbaec63d19cbfd74a074b2c5f742',
|
||||
consumerSecret: 'cs_0946bbbeea1bfefff08a69e817ac62a48412df8c',
|
||||
siteName: 'Local',
|
||||
email: '2469687281@qq.com',
|
||||
emailPswd: 'lulin91.',
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
wpApiUrl: 'http://t1-shop.local/',
|
||||
consumerKey: 'ck_a369473a6451dbaec63d19cbfd74a074b2c5f742',
|
||||
consumerSecret: 'cs_0946bbbeea1bfefff08a69e817ac62a48412df8c',
|
||||
siteName: 'Local-test-2',
|
||||
email: '2469687281@qq.com',
|
||||
emailPswd: 'lulin91.',
|
||||
},
|
||||
// {
|
||||
// id: '2',
|
||||
// wpApiUrl: 'http://localhost:10004',
|
||||
// consumerKey: 'ck_dc9e151e9048c8ed3e27f35ac79d2bf7d6840652',
|
||||
// consumerSecret: 'cs_d05d625d7b0ac05c6d765671d8417f41d9477e38',
|
||||
// siteName: 'Local',
|
||||
// email: 'tom@yoonevape.com',
|
||||
// emailPswd: 'lulin91.',
|
||||
// },
|
||||
],
|
||||
freightcom: {
|
||||
url: 'https://customer-external-api.ssd-test.freightcom.com',
|
||||
token: '6zGj1qPTL1jIkbLmgaiYc6SwHUIXJ2t25htUF8uuFYiCg8ILCY6xnBEbvrX1p79L',
|
||||
|
|
|
|||
|
|
@ -81,8 +81,5 @@ export class MainConfiguration {
|
|||
}
|
||||
}
|
||||
);
|
||||
|
||||
const sites = this.app.getConfig('wpSite') || [];
|
||||
await this.siteService.syncFromConfig(sites);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { successResponse, errorResponse } from '../utils/response.util';
|
|||
import { CustomerService } from '../service/customer.service';
|
||||
import { QueryCustomerListDTO, CustomerTagDTO } from '../dto/customer.dto';
|
||||
import { ApiOkResponse } from '@midwayjs/swagger';
|
||||
import { UnifiedSearchParamsDTO } from '../dto/site-api.dto';
|
||||
|
||||
@Controller('/customer')
|
||||
export class CustomerController {
|
||||
|
|
@ -13,7 +14,18 @@ export class CustomerController {
|
|||
@Get('/getcustomerlist')
|
||||
async getCustomerList(@Query() query: QueryCustomerListDTO) {
|
||||
try {
|
||||
const result = await this.customerService.getCustomerList(query as any);
|
||||
const result = await this.customerService.getCustomerList(query)
|
||||
return successResponse(result);
|
||||
} catch (error) {
|
||||
return errorResponse(error.message);
|
||||
}
|
||||
}
|
||||
|
||||
@ApiOkResponse({ type: Object })
|
||||
@Get('/getcustomerstatisticlist')
|
||||
async getCustomerStatisticList(@Query() query: QueryCustomerListDTO) {
|
||||
try {
|
||||
const result = await this.customerService.getCustomerStatisticList(query as any);
|
||||
return successResponse(result);
|
||||
} catch (error) {
|
||||
return errorResponse(error.message);
|
||||
|
|
@ -63,4 +75,24 @@ export class CustomerController {
|
|||
return errorResponse(error.message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 同步客户数据
|
||||
* 从指定站点获取客户数据并保存到本地数据库
|
||||
* 业务逻辑已移到service层,controller只负责参数传递和响应
|
||||
*/
|
||||
@ApiOkResponse({ type: Object })
|
||||
@Post('/sync')
|
||||
async syncCustomers(@Body() body: { siteId: number; params?: UnifiedSearchParamsDTO }) {
|
||||
try {
|
||||
const { siteId, params = {} } = body;
|
||||
|
||||
// 调用service层的同步方法,所有业务逻辑都在service中处理
|
||||
const syncResult = await this.customerService.syncCustomersFromSite(siteId, params);
|
||||
|
||||
return successResponse(syncResult);
|
||||
} catch (error) {
|
||||
return errorResponse(error.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -253,21 +253,6 @@ export class ProductController {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
// 获取所有 WordPress 商品
|
||||
@ApiOkResponse({ description: '获取所有 WordPress 商品' })
|
||||
@Get('/wp-products')
|
||||
async getWpProducts() {
|
||||
try {
|
||||
const data = await this.productService.getWpProducts();
|
||||
return successResponse(data);
|
||||
} catch (error) {
|
||||
return errorResponse(error?.message || error);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// 通用属性接口:分页列表
|
||||
@ApiOkResponse()
|
||||
@Get('/attribute')
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ import {
|
|||
CancelShipOrderDTO,
|
||||
BatchShipOrdersDTO,
|
||||
} from '../dto/site-api.dto';
|
||||
import { BatchOperationDTO, BatchOperationResultDTO } from '../dto/batch.dto';
|
||||
import { SiteApiService } from '../service/site-api.service';
|
||||
import { errorResponse, successResponse } from '../utils/response.util';
|
||||
import { ILogger } from '@midwayjs/core';
|
||||
|
|
@ -270,8 +271,8 @@ export class SiteApiController {
|
|||
page += 1;
|
||||
}
|
||||
let items = all;
|
||||
if (query.ids) {
|
||||
const ids = new Set(String(query.ids).split(',').map(v => v.trim()).filter(Boolean));
|
||||
if (query.where?.ids) {
|
||||
const ids = new Set(String(query.where.ids).split(',').map(v => v.trim()).filter(Boolean));
|
||||
items = items.filter(i => ids.has(String(i.id)));
|
||||
}
|
||||
const header = ['id', 'name', 'type', 'status', 'sku', 'regular_price', 'sale_price', 'price', 'stock_status', 'stock_quantity', 'image_src'];
|
||||
|
|
@ -533,10 +534,10 @@ export class SiteApiController {
|
|||
}
|
||||
|
||||
@Post('/:siteId/products/batch')
|
||||
@ApiOkResponse({ type: Object })
|
||||
@ApiOkResponse({ type: BatchOperationResultDTO })
|
||||
async batchProducts(
|
||||
@Param('siteId') siteId: number,
|
||||
@Body() body: { create?: any[]; update?: any[]; delete?: Array<string | number> }
|
||||
@Body() body: BatchOperationDTO
|
||||
) {
|
||||
this.logger.info(`[Site API] 批量处理产品开始, siteId: ${siteId}`);
|
||||
try {
|
||||
|
|
@ -549,14 +550,18 @@ export class SiteApiController {
|
|||
const created: any[] = [];
|
||||
const updated: any[] = [];
|
||||
const deleted: Array<string | number> = [];
|
||||
const failed: any[] = [];
|
||||
const errors: Array<{identifier: string, error: string}> = [];
|
||||
|
||||
if (body.create?.length) {
|
||||
for (const item of body.create) {
|
||||
try {
|
||||
const data = await adapter.createProduct(item);
|
||||
created.push(data);
|
||||
} catch (e) {
|
||||
failed.push({ action: 'create', item, error: (e as any).message });
|
||||
errors.push({
|
||||
identifier: String(item.id || item.sku || 'unknown'),
|
||||
error: (e as any).message
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -567,7 +572,10 @@ export class SiteApiController {
|
|||
const data = await adapter.updateProduct(id, item);
|
||||
updated.push(data);
|
||||
} catch (e) {
|
||||
failed.push({ action: 'update', item, error: (e as any).message });
|
||||
errors.push({
|
||||
identifier: String(item.id || 'unknown'),
|
||||
error: (e as any).message
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -576,14 +584,28 @@ export class SiteApiController {
|
|||
try {
|
||||
const ok = await adapter.deleteProduct(id);
|
||||
if (ok) deleted.push(id);
|
||||
else failed.push({ action: 'delete', id, error: 'delete failed' });
|
||||
else errors.push({
|
||||
identifier: String(id),
|
||||
error: 'delete failed'
|
||||
});
|
||||
} catch (e) {
|
||||
failed.push({ action: 'delete', id, error: (e as any).message });
|
||||
errors.push({
|
||||
identifier: String(id),
|
||||
error: (e as any).message
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.logger.info(`[Site API] 批量处理产品完成, siteId: ${siteId}`);
|
||||
return successResponse({ created, updated, deleted, failed });
|
||||
return successResponse({
|
||||
total: (body.create?.length || 0) + (body.update?.length || 0) + (body.delete?.length || 0),
|
||||
processed: created.length + updated.length + deleted.length,
|
||||
created: created.length,
|
||||
updated: updated.length,
|
||||
deleted: deleted.length,
|
||||
errors: errors
|
||||
});
|
||||
} catch (error) {
|
||||
this.logger.error(`[Site API] 批量处理产品失败, siteId: ${siteId}, 错误信息: ${error.message}`);
|
||||
return errorResponse(error.message);
|
||||
|
|
@ -600,10 +622,6 @@ export class SiteApiController {
|
|||
try {
|
||||
const adapter = await this.siteApiService.getAdapter(siteId);
|
||||
const where = { ...(query.where || {}) };
|
||||
if (query.customer_id) {
|
||||
where.customer = query.customer_id;
|
||||
where.customer_id = query.customer_id;
|
||||
}
|
||||
const data = await adapter.getOrders({ ...query, where });
|
||||
this.logger.info(`[Site API] 获取订单列表成功, siteId: ${siteId}, 共获取到 ${data.total} 个订单`);
|
||||
return successResponse(data);
|
||||
|
|
@ -623,8 +641,8 @@ export class SiteApiController {
|
|||
this.logger.info(`[Site API] 获取客户订单列表开始, siteId: ${siteId}, customerId: ${customerId}, query: ${JSON.stringify(query)}`);
|
||||
try {
|
||||
const adapter = await this.siteApiService.getAdapter(siteId);
|
||||
const where = { ...(query.where || {}), customer: customerId, customer_id: customerId };
|
||||
const data = await adapter.getOrders({ ...query, where, customer_id: customerId });
|
||||
const where = { ...(query.where || {}), customer: customerId };
|
||||
const data = await adapter.getOrders({ ...query, where });
|
||||
this.logger.info(`[Site API] 获取客户订单列表成功, siteId: ${siteId}, customerId: ${customerId}, 共获取到 ${data.total} 个订单`);
|
||||
return successResponse(data);
|
||||
} catch (error) {
|
||||
|
|
@ -652,8 +670,8 @@ export class SiteApiController {
|
|||
page += 1;
|
||||
}
|
||||
let items = all;
|
||||
if (query.ids) {
|
||||
const ids = new Set(String(query.ids).split(',').map(v => v.trim()).filter(Boolean));
|
||||
if (query.where?.ids) {
|
||||
const ids = new Set(String(query.where.ids).split(',').map(v => v.trim()).filter(Boolean));
|
||||
items = items.filter(i => ids.has(String(i.id)));
|
||||
}
|
||||
const header = ['id', 'number', 'status', 'currency', 'total', 'customer_id', 'customer_name', 'email', 'payment_method', 'phone', 'billing_full_address', 'shipping_full_address', 'date_created'];
|
||||
|
|
@ -793,10 +811,10 @@ export class SiteApiController {
|
|||
}
|
||||
|
||||
@Post('/:siteId/orders/batch')
|
||||
@ApiOkResponse({ type: Object })
|
||||
@ApiOkResponse({ type: BatchOperationResultDTO })
|
||||
async batchOrders(
|
||||
@Param('siteId') siteId: number,
|
||||
@Body() body: { create?: any[]; update?: any[]; delete?: Array<string | number> }
|
||||
@Body() body: BatchOperationDTO
|
||||
) {
|
||||
this.logger.info(`[Site API] 批量处理订单开始, siteId: ${siteId}`);
|
||||
try {
|
||||
|
|
@ -804,14 +822,18 @@ export class SiteApiController {
|
|||
const created: any[] = [];
|
||||
const updated: any[] = [];
|
||||
const deleted: Array<string | number> = [];
|
||||
const failed: any[] = [];
|
||||
const errors: Array<{identifier: string, error: string}> = [];
|
||||
|
||||
if (body.create?.length) {
|
||||
for (const item of body.create) {
|
||||
try {
|
||||
const data = await adapter.createOrder(item);
|
||||
created.push(data);
|
||||
} catch (e) {
|
||||
failed.push({ action: 'create', item, error: (e as any).message });
|
||||
errors.push({
|
||||
identifier: String(item.id || item.order_number || 'unknown'),
|
||||
error: (e as any).message
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -821,9 +843,15 @@ export class SiteApiController {
|
|||
const id = item.id;
|
||||
const ok = await adapter.updateOrder(id, item);
|
||||
if (ok) updated.push(item);
|
||||
else failed.push({ action: 'update', item, error: 'update failed' });
|
||||
else errors.push({
|
||||
identifier: String(item.id || 'unknown'),
|
||||
error: 'update failed'
|
||||
});
|
||||
} catch (e) {
|
||||
failed.push({ action: 'update', item, error: (e as any).message });
|
||||
errors.push({
|
||||
identifier: String(item.id || 'unknown'),
|
||||
error: (e as any).message
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -832,14 +860,28 @@ export class SiteApiController {
|
|||
try {
|
||||
const ok = await adapter.deleteOrder(id);
|
||||
if (ok) deleted.push(id);
|
||||
else failed.push({ action: 'delete', id, error: 'delete failed' });
|
||||
else errors.push({
|
||||
identifier: String(id),
|
||||
error: 'delete failed'
|
||||
});
|
||||
} catch (e) {
|
||||
failed.push({ action: 'delete', id, error: (e as any).message });
|
||||
errors.push({
|
||||
identifier: String(id),
|
||||
error: (e as any).message
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.logger.info(`[Site API] 批量处理订单完成, siteId: ${siteId}`);
|
||||
return successResponse({ created, updated, deleted, failed });
|
||||
return successResponse({
|
||||
total: (body.create?.length || 0) + (body.update?.length || 0) + (body.delete?.length || 0),
|
||||
processed: created.length + updated.length + deleted.length,
|
||||
created: created.length,
|
||||
updated: updated.length,
|
||||
deleted: deleted.length,
|
||||
errors: errors
|
||||
});
|
||||
} catch (error) {
|
||||
this.logger.error(`[Site API] 批量处理订单失败, siteId: ${siteId}, 错误信息: ${error.message}`);
|
||||
return errorResponse(error.message);
|
||||
|
|
@ -1005,8 +1047,8 @@ export class SiteApiController {
|
|||
page += 1;
|
||||
}
|
||||
let items = all;
|
||||
if (query.ids) {
|
||||
const ids = new Set(String(query.ids).split(',').map(v => v.trim()).filter(Boolean));
|
||||
if (query.where?.ids) {
|
||||
const ids = new Set(String(query.where.ids).split(',').map(v => v.trim()).filter(Boolean));
|
||||
items = items.filter(i => ids.has(String(i.id)));
|
||||
}
|
||||
const header = ['id', 'status', 'customer_id', 'billing_period', 'billing_interval', 'start_date', 'next_payment_date'];
|
||||
|
|
@ -1055,8 +1097,8 @@ export class SiteApiController {
|
|||
page += 1;
|
||||
}
|
||||
let items = all;
|
||||
if (query.ids) {
|
||||
const ids = new Set(String(query.ids).split(',').map(v => v.trim()).filter(Boolean));
|
||||
if (query.where?.ids) {
|
||||
const ids = new Set(String(query.where.ids).split(',').map(v => v.trim()).filter(Boolean));
|
||||
items = items.filter(i => ids.has(String(i.id)));
|
||||
}
|
||||
const header = ['id', 'title', 'media_type', 'mime_type', 'source_url', 'date_created'];
|
||||
|
|
@ -1220,10 +1262,6 @@ export class SiteApiController {
|
|||
page += 1;
|
||||
}
|
||||
let items = all;
|
||||
if (query.ids) {
|
||||
const ids = new Set(String(query.ids).split(',').map(v => v.trim()).filter(Boolean));
|
||||
items = items.filter(i => ids.has(String(i.id)));
|
||||
}
|
||||
const header = ['id', 'email', 'first_name', 'last_name', 'fullname', 'username', 'phone', 'orders', 'total_spend', 'role', 'billing_full_address', 'shipping_full_address', 'date_created'];
|
||||
const formatAddress = (addr: any) => [
|
||||
addr?.fullname,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { Body, Controller, Get, Inject, Param, Put, Post, Query } from '@midwayjs/core';
|
||||
import { ApiOkResponse } from '@midwayjs/swagger';
|
||||
import { WpSitesResponse } from '../dto/reponse.dto';
|
||||
import { SitesResponse } from '../dto/reponse.dto';
|
||||
import { errorResponse, successResponse } from '../utils/response.util';
|
||||
import { SiteService } from '../service/site.service';
|
||||
import { CreateSiteDTO, DisableSiteDTO, QuerySiteDTO, UpdateSiteDTO } from '../dto/site.dto';
|
||||
|
|
@ -10,7 +10,7 @@ export class SiteController {
|
|||
@Inject()
|
||||
siteService: SiteService;
|
||||
|
||||
@ApiOkResponse({ description: '关联网站', type: WpSitesResponse })
|
||||
@ApiOkResponse({ description: '关联网站', type: SitesResponse })
|
||||
@Get('/all')
|
||||
async all() {
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { HttpStatus, Inject } from '@midwayjs/core';
|
||||
import { HttpStatus, ILogger, Inject, Logger } from '@midwayjs/core';
|
||||
import {
|
||||
Controller,
|
||||
Post,
|
||||
|
|
@ -25,6 +25,9 @@ export class WebhookController {
|
|||
@Inject()
|
||||
ctx: Context;
|
||||
|
||||
@Logger()
|
||||
logger: ILogger;
|
||||
|
||||
@Inject()
|
||||
private readonly siteService: SiteService;
|
||||
|
||||
|
|
@ -48,7 +51,7 @@ export class WebhookController {
|
|||
// 从数据库获取站点配置
|
||||
const site = await this.siteService.get(siteId, true);
|
||||
|
||||
if (!site || !source.includes(site.apiUrl)) {
|
||||
if (!site || !source?.includes(site.apiUrl)) {
|
||||
console.log('domain not match');
|
||||
return {
|
||||
code: HttpStatus.BAD_REQUEST,
|
||||
|
|
|
|||
|
|
@ -1,232 +0,0 @@
|
|||
import {
|
||||
Controller,
|
||||
Param,
|
||||
Post,
|
||||
Inject,
|
||||
Get,
|
||||
Query,
|
||||
Put,
|
||||
Body,
|
||||
Files,
|
||||
Del,
|
||||
} from '@midwayjs/core';
|
||||
import { WpProductService } from '../service/wp_product.service';
|
||||
import { errorResponse, successResponse } from '../utils/response.util';
|
||||
import { ApiOkResponse } from '@midwayjs/swagger';
|
||||
import { BooleanRes, WpProductListRes } from '../dto/reponse.dto';
|
||||
import {
|
||||
QueryWpProductDTO,
|
||||
UpdateVariationDTO,
|
||||
UpdateWpProductDTO,
|
||||
BatchSyncProductsDTO,
|
||||
BatchUpdateTagsDTO,
|
||||
BatchUpdateProductsDTO,
|
||||
} from '../dto/wp_product.dto';
|
||||
|
||||
import {
|
||||
ProductsRes,
|
||||
} from '../dto/reponse.dto';
|
||||
@Controller('/wp_product')
|
||||
export class WpProductController {
|
||||
// 移除控制器内的配置站点引用,统一由服务层处理站点数据
|
||||
|
||||
@Inject()
|
||||
private readonly wpProductService: WpProductService;
|
||||
|
||||
// 平台服务保留按需注入
|
||||
|
||||
@ApiOkResponse({
|
||||
type: BooleanRes,
|
||||
})
|
||||
@Del('/:id')
|
||||
async delete(@Param('id') id: number) {
|
||||
return errorResponse('接口已废弃,请改用 /site-api/:siteId/products 删除');
|
||||
}
|
||||
|
||||
@ApiOkResponse({
|
||||
type: BooleanRes,
|
||||
})
|
||||
@Post('/import/:siteId')
|
||||
async importProducts(@Param('siteId') siteId: number, @Files() files) {
|
||||
try {
|
||||
if (!files || files.length === 0) {
|
||||
throw new Error('请上传文件');
|
||||
}
|
||||
await this.wpProductService.importProducts(siteId, files[0]);
|
||||
return successResponse(true);
|
||||
} catch (error) {
|
||||
console.error('导入失败:', error);
|
||||
return errorResponse(error.message || '导入失败');
|
||||
}
|
||||
}
|
||||
|
||||
@ApiOkResponse({
|
||||
type: BooleanRes,
|
||||
})
|
||||
@Post('/setconstitution')
|
||||
async setConstitution(@Body() body: any) {
|
||||
try {
|
||||
return successResponse(true);
|
||||
} catch (error) {
|
||||
return errorResponse(error.message || '设置失败');
|
||||
}
|
||||
}
|
||||
|
||||
@ApiOkResponse({
|
||||
type: BooleanRes,
|
||||
})
|
||||
@Post('/batch-update')
|
||||
async batchUpdateProducts(@Body() body: BatchUpdateProductsDTO) {
|
||||
try {
|
||||
await this.wpProductService.batchUpdateProducts(body);
|
||||
return successResponse(true);
|
||||
} catch (error) {
|
||||
return errorResponse(error.message || '批量更新失败');
|
||||
}
|
||||
}
|
||||
|
||||
@ApiOkResponse({
|
||||
type: BooleanRes,
|
||||
})
|
||||
@Post('/batch-update-tags')
|
||||
async batchUpdateTags(@Body() body: BatchUpdateTagsDTO) {
|
||||
try {
|
||||
await this.wpProductService.batchUpdateTags(body.ids, body.tags);
|
||||
return successResponse(true);
|
||||
} catch (error) {
|
||||
return errorResponse(error.message || '批量更新标签失败');
|
||||
}
|
||||
}
|
||||
|
||||
@ApiOkResponse({
|
||||
type: BooleanRes,
|
||||
})
|
||||
@Post('/sync/:siteId')
|
||||
async syncProducts(@Param('siteId') siteId: number) {
|
||||
try {
|
||||
const result = await this.wpProductService.syncSite(siteId);
|
||||
return successResponse(result);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
return errorResponse('同步失败');
|
||||
}
|
||||
}
|
||||
|
||||
@ApiOkResponse({
|
||||
type: BooleanRes,
|
||||
})
|
||||
@Post('/batch-sync-to-site/:siteId')
|
||||
async batchSyncToSite(
|
||||
@Param('siteId') siteId: number,
|
||||
@Body() body: BatchSyncProductsDTO
|
||||
) {
|
||||
try {
|
||||
await this.wpProductService.batchSyncToSite(siteId, body.productIds);
|
||||
return successResponse(true, '批量同步成功');
|
||||
} catch (error) {
|
||||
console.error('批量同步失败:', error);
|
||||
return errorResponse(error.message || '批量同步失败');
|
||||
}
|
||||
}
|
||||
|
||||
@ApiOkResponse({
|
||||
type: WpProductListRes,
|
||||
})
|
||||
@Get('/list')
|
||||
async getWpProducts(@Query() query: QueryWpProductDTO) {
|
||||
return errorResponse('接口已废弃,请改用 /site-api/:siteId/products 列表');
|
||||
}
|
||||
|
||||
@ApiOkResponse({
|
||||
type: BooleanRes
|
||||
})
|
||||
@Post('/updateState/:id')
|
||||
async updateWPProductState(
|
||||
@Param('id') id: number,
|
||||
@Body() body: any, // todo
|
||||
) {
|
||||
try {
|
||||
const res = await this.wpProductService.updateProductStatus(id, body?.status, body?.stock_status);
|
||||
return successResponse(res);
|
||||
} catch (error) {
|
||||
return errorResponse(error.message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建产品接口
|
||||
* @param siteId 站点 ID
|
||||
* @param body 创建数据
|
||||
*/
|
||||
@ApiOkResponse({
|
||||
type: BooleanRes,
|
||||
})
|
||||
@Post('/siteId/:siteId/products')
|
||||
async createProduct(
|
||||
@Param('siteId') siteId: number,
|
||||
@Body() body: any
|
||||
) {
|
||||
return errorResponse('接口已废弃,请改用 /site-api/:siteId/products 创建');
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新产品接口
|
||||
* @param productId 产品 ID
|
||||
* @param body 更新数据
|
||||
*/
|
||||
@ApiOkResponse({
|
||||
type: BooleanRes,
|
||||
})
|
||||
@Put('/siteId/:siteId/products/:productId')
|
||||
async updateProduct(
|
||||
@Param('siteId') siteId: number,
|
||||
@Param('productId') productId: string,
|
||||
@Body() body: UpdateWpProductDTO
|
||||
) {
|
||||
return errorResponse('接口已废弃,请改用 /site-api/:siteId/products/:id 更新');
|
||||
}
|
||||
|
||||
@ApiOkResponse({
|
||||
type: BooleanRes,
|
||||
})
|
||||
@Post('/sync-to-product/:id')
|
||||
async syncToProduct(@Param('id') id: number) {
|
||||
try {
|
||||
await this.wpProductService.syncToProduct(id);
|
||||
return successResponse(true);
|
||||
} catch (error) {
|
||||
return errorResponse(error.message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新变体接口
|
||||
* @param productId 产品 ID
|
||||
* @param variationId 变体 ID
|
||||
* @param body 更新数据
|
||||
*/
|
||||
@Put('/siteId/:siteId/products/:productId/variations/:variationId')
|
||||
async updateVariation(
|
||||
@Param('siteId') siteId: number,
|
||||
@Param('productId') productId: string,
|
||||
@Param('variationId') variationId: string,
|
||||
@Body() body: UpdateVariationDTO
|
||||
) {
|
||||
return errorResponse('接口已废弃,请改用 /site-api/:siteId/products/:productId/variations/:variationId 更新');
|
||||
}
|
||||
|
||||
@ApiOkResponse({
|
||||
description: '通过name搜索产品/订单',
|
||||
type: ProductsRes,
|
||||
})
|
||||
@Get('/search')
|
||||
async searchProducts(@Query('name') name: string) {
|
||||
try {
|
||||
// 调用服务获取产品数据
|
||||
const products = await this.wpProductService.findProductsByName(name);
|
||||
return successResponse(products);
|
||||
} catch (error) {
|
||||
return errorResponse(error.message || '获取数据失败');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,91 @@
|
|||
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||
|
||||
export class UpdateProductTableName1765358400000 implements MigrationInterface {
|
||||
name = 'UpdateProductTableName1765358400000'
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
// 1. 使用 try-catch 方式删除外键约束,避免因外键不存在导致迁移失败
|
||||
try {
|
||||
await queryRunner.query("ALTER TABLE `product_attributes_dict_item` DROP FOREIGN KEY `FK_592cdbdaebfec346c202ffb82ca`");
|
||||
} catch (error) {
|
||||
// 忽略外键不存在的错误
|
||||
console.log('Warning: Failed to drop foreign key on product_attributes_dict_item. It may not exist.');
|
||||
}
|
||||
|
||||
try {
|
||||
await queryRunner.query("ALTER TABLE `product_stock_component` DROP FOREIGN KEY `FK_6fe75663083f572a49e7f46909b`");
|
||||
} catch (error) {
|
||||
console.log('Warning: Failed to drop foreign key on product_stock_component. It may not exist.');
|
||||
}
|
||||
|
||||
try {
|
||||
await queryRunner.query("ALTER TABLE `product_site_sku` DROP FOREIGN KEY `FK_3b9b7f3d8a6d9f3e2c0b1a4d5e6`");
|
||||
} catch (error) {
|
||||
console.log('Warning: Failed to drop foreign key on product_site_sku. It may not exist.');
|
||||
}
|
||||
|
||||
try {
|
||||
await queryRunner.query("ALTER TABLE `order_sale` DROP FOREIGN KEY `FK_order_sale_product`");
|
||||
} catch (error) {
|
||||
console.log('Warning: Failed to drop foreign key on order_sale. It may not exist.');
|
||||
}
|
||||
|
||||
// 2. 将 product 表重命名为 product_v2
|
||||
await queryRunner.query("ALTER TABLE `product` RENAME TO `product_v2`");
|
||||
|
||||
// 3. 重新创建所有外键约束,引用新的 product_v2 表
|
||||
await queryRunner.query("ALTER TABLE `product_attributes_dict_item` ADD CONSTRAINT `FK_592cdbdaebfec346c202ffb82ca` FOREIGN KEY (`productId`) REFERENCES `product_v2`(`id`) ON DELETE CASCADE ON UPDATE CASCADE");
|
||||
await queryRunner.query("ALTER TABLE `product_stock_component` ADD CONSTRAINT `FK_6fe75663083f572a49e7f46909b` FOREIGN KEY (`productId`) REFERENCES `product_v2`(`id`) ON DELETE CASCADE");
|
||||
await queryRunner.query("ALTER TABLE `product_site_sku` ADD CONSTRAINT `FK_3b9b7f3d8a6d9f3e2c0b1a4d5e6` FOREIGN KEY (`productId`) REFERENCES `product_v2`(`id`) ON DELETE CASCADE");
|
||||
|
||||
// 4. 为 order_sale 表添加外键约束
|
||||
try {
|
||||
await queryRunner.query("ALTER TABLE `order_sale` ADD CONSTRAINT `FK_order_sale_product` FOREIGN KEY (`productId`) REFERENCES `product_v2`(`id`) ON DELETE CASCADE");
|
||||
} catch (error) {
|
||||
console.log('Warning: Failed to add foreign key on order_sale. It may already exist.');
|
||||
}
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
// 回滚操作
|
||||
// 1. 删除外键约束
|
||||
try {
|
||||
await queryRunner.query("ALTER TABLE `product_attributes_dict_item` DROP FOREIGN KEY `FK_592cdbdaebfec346c202ffb82ca`");
|
||||
} catch (error) {
|
||||
console.log('Warning: Failed to drop foreign key on product_attributes_dict_item during rollback.');
|
||||
}
|
||||
|
||||
try {
|
||||
await queryRunner.query("ALTER TABLE `product_stock_component` DROP FOREIGN KEY `FK_6fe75663083f572a49e7f46909b`");
|
||||
} catch (error) {
|
||||
console.log('Warning: Failed to drop foreign key on product_stock_component during rollback.');
|
||||
}
|
||||
|
||||
try {
|
||||
await queryRunner.query("ALTER TABLE `product_site_sku` DROP FOREIGN KEY `FK_3b9b7f3d8a6d9f3e2c0b1a4d5e6`");
|
||||
} catch (error) {
|
||||
console.log('Warning: Failed to drop foreign key on product_site_sku during rollback.');
|
||||
}
|
||||
|
||||
try {
|
||||
await queryRunner.query("ALTER TABLE `order_sale` DROP FOREIGN KEY `FK_order_sale_product`");
|
||||
} catch (error) {
|
||||
console.log('Warning: Failed to drop foreign key on order_sale during rollback.');
|
||||
}
|
||||
|
||||
// 2. 将 product_v2 表重命名回 product
|
||||
await queryRunner.query("ALTER TABLE `product_v2` RENAME TO `product`");
|
||||
|
||||
// 3. 重新创建外键约束,引用回原来的 product 表
|
||||
await queryRunner.query("ALTER TABLE `product_attributes_dict_item` ADD CONSTRAINT `FK_592cdbdaebfec346c202ffb82ca` FOREIGN KEY (`productId`) REFERENCES `product`(`id`) ON DELETE CASCADE ON UPDATE CASCADE");
|
||||
await queryRunner.query("ALTER TABLE `product_stock_component` ADD CONSTRAINT `FK_6fe75663083f572a49e7f46909b` FOREIGN KEY (`productId`) REFERENCES `product`(`id`) ON DELETE CASCADE");
|
||||
await queryRunner.query("ALTER TABLE `product_site_sku` ADD CONSTRAINT `FK_3b9b7f3d8a6d9f3e2c0b1a4d5e6` FOREIGN KEY (`productId`) REFERENCES `product`(`id`) ON DELETE CASCADE");
|
||||
|
||||
// 4. 为 order_sale 表重新创建外键约束
|
||||
try {
|
||||
await queryRunner.query("ALTER TABLE `order_sale` ADD CONSTRAINT `FK_order_sale_product` FOREIGN KEY (`productId`) REFERENCES `product`(`id`) ON DELETE CASCADE");
|
||||
} catch (error) {
|
||||
console.log('Warning: Failed to add foreign key on order_sale during rollback. It may already exist.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,210 @@
|
|||
import { ApiProperty } from '@midwayjs/swagger';
|
||||
import { Rule, RuleType } from '@midwayjs/validate';
|
||||
|
||||
/**
|
||||
* 批量操作错误项
|
||||
*/
|
||||
export interface BatchErrorItem {
|
||||
// 错误项标识(可以是ID、邮箱等)
|
||||
identifier: string;
|
||||
// 错误信息
|
||||
error: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量操作结果基础接口
|
||||
*/
|
||||
export interface BatchOperationResult {
|
||||
// 总处理数量
|
||||
total: number;
|
||||
// 成功处理数量
|
||||
processed: number;
|
||||
// 创建数量
|
||||
created?: number;
|
||||
// 更新数量
|
||||
updated?: number;
|
||||
// 删除数量
|
||||
deleted?: number;
|
||||
// 跳过的数量(如数据已存在或无需处理)
|
||||
skipped?: number;
|
||||
// 错误列表
|
||||
errors: BatchErrorItem[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 同步操作结果接口
|
||||
*/
|
||||
export interface SyncOperationResult extends BatchOperationResult {
|
||||
// 同步成功数量
|
||||
synced: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量操作错误项DTO
|
||||
*/
|
||||
export class BatchErrorItemDTO {
|
||||
@ApiProperty({ description: '错误项标识(如ID、邮箱等)', type: String })
|
||||
@Rule(RuleType.string().required())
|
||||
identifier: string;
|
||||
|
||||
@ApiProperty({ description: '错误信息', type: String })
|
||||
@Rule(RuleType.string().required())
|
||||
error: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量操作结果基础DTO
|
||||
*/
|
||||
export class BatchOperationResultDTO {
|
||||
@ApiProperty({ description: '总处理数量', type: Number })
|
||||
total: number;
|
||||
|
||||
@ApiProperty({ description: '成功处理数量', type: Number })
|
||||
processed: number;
|
||||
|
||||
@ApiProperty({ description: '创建数量', type: Number, required: false })
|
||||
created?: number;
|
||||
|
||||
@ApiProperty({ description: '更新数量', type: Number, required: false })
|
||||
updated?: number;
|
||||
|
||||
@ApiProperty({ description: '删除数量', type: Number, required: false })
|
||||
deleted?: number;
|
||||
|
||||
@ApiProperty({ description: '跳过的数量', type: Number, required: false })
|
||||
skipped?: number;
|
||||
|
||||
@ApiProperty({ description: '错误列表', type: [BatchErrorItemDTO] })
|
||||
errors: BatchErrorItemDTO[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 同步操作结果DTO
|
||||
*/
|
||||
export class SyncOperationResultDTO extends BatchOperationResultDTO {
|
||||
@ApiProperty({ description: '同步成功数量', type: Number })
|
||||
synced: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量创建DTO
|
||||
*/
|
||||
export class BatchCreateDTO<T = any> {
|
||||
@ApiProperty({ description: '要创建的数据列表', type: Array })
|
||||
@Rule(RuleType.array().required())
|
||||
items: T[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量更新DTO
|
||||
*/
|
||||
export class BatchUpdateDTO<T = any> {
|
||||
@ApiProperty({ description: '要更新的数据列表', type: Array })
|
||||
@Rule(RuleType.array().required())
|
||||
items: T[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量删除DTO
|
||||
*/
|
||||
export class BatchDeleteDTO {
|
||||
@ApiProperty({ description: '要删除的ID列表', type: [String, Number] })
|
||||
@Rule(RuleType.array().items(RuleType.alternatives().try(RuleType.string(), RuleType.number())).required())
|
||||
ids: Array<string | number>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量操作请求DTO(包含增删改)
|
||||
*/
|
||||
export class BatchOperationDTO<T = any> {
|
||||
@ApiProperty({ description: '要创建的数据列表', type: Array, required: false })
|
||||
@Rule(RuleType.array().optional())
|
||||
create?: T[];
|
||||
|
||||
@ApiProperty({ description: '要更新的数据列表', type: Array, required: false })
|
||||
@Rule(RuleType.array().optional())
|
||||
update?: T[];
|
||||
|
||||
@ApiProperty({ description: '要删除的ID列表', type: [String, Number], required: false })
|
||||
@Rule(RuleType.array().items(RuleType.alternatives().try(RuleType.string(), RuleType.number())).optional())
|
||||
delete?: Array<string | number>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页批量操作DTO
|
||||
*/
|
||||
export class PaginatedBatchOperationDTO<T = any> {
|
||||
@ApiProperty({ description: '页码', type: Number, required: false, default: 1 })
|
||||
@Rule(RuleType.number().integer().min(1).optional())
|
||||
page?: number = 1;
|
||||
|
||||
@ApiProperty({ description: '每页数量', type: Number, required: false, default: 100 })
|
||||
@Rule(RuleType.number().integer().min(1).max(1000).optional())
|
||||
pageSize?: number = 100;
|
||||
|
||||
@ApiProperty({ description: '要创建的数据列表', type: Array, required: false })
|
||||
@Rule(RuleType.array().optional())
|
||||
create?: T[];
|
||||
|
||||
@ApiProperty({ description: '要更新的数据列表', type: Array, required: false })
|
||||
@Rule(RuleType.array().optional())
|
||||
update?: T[];
|
||||
|
||||
@ApiProperty({ description: '要删除的ID列表', type: [String, Number], required: false })
|
||||
@Rule(RuleType.array().items(RuleType.alternatives().try(RuleType.string(), RuleType.number())).optional())
|
||||
delete?: Array<string | number>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 同步参数DTO
|
||||
*/
|
||||
export class SyncParamsDTO {
|
||||
@ApiProperty({ description: '页码', type: Number, required: false, default: 1 })
|
||||
@Rule(RuleType.number().integer().min(1).optional())
|
||||
page?: number = 1;
|
||||
|
||||
@ApiProperty({ description: '每页数量', type: Number, required: false, default: 100 })
|
||||
@Rule(RuleType.number().integer().min(1).max(1000).optional())
|
||||
pageSize?: number = 100;
|
||||
|
||||
@ApiProperty({ description: '开始时间', type: String, required: false })
|
||||
@Rule(RuleType.string().optional())
|
||||
startDate?: string;
|
||||
|
||||
@ApiProperty({ description: '结束时间', type: String, required: false })
|
||||
@Rule(RuleType.string().optional())
|
||||
endDate?: string;
|
||||
|
||||
@ApiProperty({ description: '强制同步(忽略缓存)', type: Boolean, required: false, default: false })
|
||||
@Rule(RuleType.boolean().optional())
|
||||
force?: boolean = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量查询DTO
|
||||
*/
|
||||
export class BatchQueryDTO {
|
||||
@ApiProperty({ description: 'ID列表', type: [String, Number] })
|
||||
@Rule(RuleType.array().items(RuleType.alternatives().try(RuleType.string(), RuleType.number())).required())
|
||||
ids: Array<string | number>;
|
||||
|
||||
@ApiProperty({ description: '包含关联数据', type: Boolean, required: false, default: false })
|
||||
@Rule(RuleType.boolean().optional())
|
||||
includeRelations?: boolean = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量操作结果类(泛型支持)
|
||||
*/
|
||||
export class BatchOperationResultDTOGeneric<T> extends BatchOperationResultDTO {
|
||||
@ApiProperty({ description: '操作成功的数据列表', type: Array })
|
||||
data?: T[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 同步操作结果类(泛型支持)
|
||||
*/
|
||||
export class SyncOperationResultDTOGeneric<T> extends SyncOperationResultDTO {
|
||||
@ApiProperty({ description: '同步成功的数据列表', type: Array })
|
||||
data?: T[];
|
||||
}
|
||||
|
|
@ -36,3 +36,35 @@ export class CustomerTagDTO {
|
|||
@ApiProperty()
|
||||
tag: string;
|
||||
}
|
||||
|
||||
export class CustomerDto {
|
||||
@ApiProperty()
|
||||
id: number;
|
||||
|
||||
@ApiProperty()
|
||||
site_id: number;
|
||||
|
||||
@ApiProperty()
|
||||
email: string;
|
||||
|
||||
@ApiProperty()
|
||||
avatar: string;
|
||||
|
||||
@ApiProperty()
|
||||
tags: string[];
|
||||
|
||||
@ApiProperty()
|
||||
rate: number;
|
||||
|
||||
@ApiProperty()
|
||||
state: string;
|
||||
|
||||
}
|
||||
|
||||
export class CustomerListResponseDTO {
|
||||
@ApiProperty()
|
||||
total: number;
|
||||
|
||||
@ApiProperty({ type: [CustomerDto] })
|
||||
list: CustomerDto[];
|
||||
}
|
||||
|
|
@ -11,7 +11,6 @@ import { OrderStatusCountDTO } from './order.dto';
|
|||
import { SiteConfig } from './site.dto';
|
||||
import { PurchaseOrderDTO, StockDTO, StockRecordDTO } from './stock.dto';
|
||||
import { LoginResDTO } from './user.dto';
|
||||
import { WpProductDTO } from './wp_product.dto';
|
||||
import { OrderSale } from '../entity/order_sale.entity';
|
||||
import { Service } from '../entity/service.entity';
|
||||
import { RateDTO } from './freightcom.dto';
|
||||
|
|
@ -25,7 +24,7 @@ import { Dict } from '../entity/dict.entity';
|
|||
|
||||
export class BooleanRes extends SuccessWrapper(Boolean) {}
|
||||
//网站配置返回数据
|
||||
export class WpSitesResponse extends SuccessArrayWrapper(SiteConfig) {}
|
||||
export class SitesResponse extends SuccessArrayWrapper(SiteConfig) {}
|
||||
//产品分页数据
|
||||
export class ProductPaginatedResponse extends PaginatedWrapper(Product) {}
|
||||
//产品分页返回数据
|
||||
|
|
@ -77,15 +76,6 @@ export class ProductSizeAllRes extends SuccessArrayWrapper(Dict) {}
|
|||
// 产品尺寸返回数据
|
||||
export class ProductSizeRes extends SuccessWrapper(Dict) {}
|
||||
|
||||
//产品分页数据
|
||||
export class WpProductPaginatedResponse extends PaginatedWrapper(
|
||||
WpProductDTO
|
||||
) {}
|
||||
//产品分页返回数据
|
||||
export class WpProductListRes extends SuccessWrapper(
|
||||
WpProductPaginatedResponse
|
||||
) {}
|
||||
|
||||
export class LoginRes extends SuccessWrapper(LoginResDTO) {}
|
||||
export class StockPaginatedRespone extends PaginatedWrapper(StockDTO) {}
|
||||
export class StockListRes extends SuccessWrapper(StockPaginatedRespone) {}
|
||||
|
|
|
|||
|
|
@ -14,9 +14,6 @@ export class UnifiedPaginationDTO<T> {
|
|||
@ApiProperty({ description: '每页数量', example: 20 })
|
||||
per_page: number;
|
||||
|
||||
@ApiProperty({ description: '每页数量别名', example: 20 })
|
||||
page_size?: number;
|
||||
|
||||
@ApiProperty({ description: '总页数', example: 5 })
|
||||
totalPages: number;
|
||||
}
|
||||
|
|
@ -529,7 +526,7 @@ export class UploadMediaDTO {
|
|||
filename: string;
|
||||
}
|
||||
|
||||
export class UnifiedSearchParamsDTO {
|
||||
export class UnifiedSearchParamsDTO<Where=Record<string, any>> {
|
||||
// 统一查询参数DTO用于承载分页与筛选与排序参数
|
||||
@ApiProperty({ description: '页码', example: 1, required: false })
|
||||
page?: number;
|
||||
|
|
@ -537,36 +534,18 @@ export class UnifiedSearchParamsDTO {
|
|||
@ApiProperty({ description: '每页数量', example: 20, required: false })
|
||||
per_page?: number;
|
||||
|
||||
@ApiProperty({ description: '每页数量别名', example: 20, required: false })
|
||||
page_size?: number;
|
||||
|
||||
@ApiProperty({ description: '搜索关键词', required: false })
|
||||
search?: string;
|
||||
|
||||
@ApiProperty({ description: '状态', required: false })
|
||||
status?: string;
|
||||
|
||||
@ApiProperty({ description: '客户ID,用于筛选订单', required: false })
|
||||
customer_id?: number;
|
||||
|
||||
@ApiProperty({ description: '过滤条件对象', type: 'object', required: false })
|
||||
where?: Record<string, any>;
|
||||
where?: Where;
|
||||
|
||||
@ApiProperty({
|
||||
description: '排序对象,例如 { "sku": "desc" }',
|
||||
type: 'object',
|
||||
required: false,
|
||||
})
|
||||
order?: Record<string, 'asc' | 'desc'> | string;
|
||||
|
||||
@ApiProperty({ description: '排序字段(兼容旧入参)', required: false })
|
||||
orderby?: string;
|
||||
|
||||
@ApiProperty({ description: '排序方式(兼容旧入参)', required: false })
|
||||
orderDir?: 'asc' | 'desc';
|
||||
|
||||
@ApiProperty({ description: '选中ID列表,逗号分隔', required: false })
|
||||
ids?: string;
|
||||
orderBy?: Record<string, 'asc' | 'desc'> | string;
|
||||
}
|
||||
|
||||
export class UnifiedWebhookDTO {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
// WooCommerce 平台原始数据类型定义
|
||||
// 仅包含当前映射逻辑所需字段以保持简洁与类型安全
|
||||
|
||||
|
||||
// 产品类型
|
||||
export interface WooProduct {
|
||||
// 产品主键
|
||||
|
|
@ -124,6 +125,9 @@ export interface WooProduct {
|
|||
// 元数据
|
||||
meta_data?: Array<{ id?: number; key: string; value: any }>;
|
||||
}
|
||||
export interface WooVariation{
|
||||
|
||||
}
|
||||
|
||||
// 订单类型
|
||||
export interface WooOrder {
|
||||
|
|
|
|||
|
|
@ -1,136 +0,0 @@
|
|||
import { ApiProperty } from '@midwayjs/swagger';
|
||||
import { Variation } from '../entity/variation.entity';
|
||||
import { WpProduct } from '../entity/wp_product.entity';
|
||||
import { Rule, RuleType } from '@midwayjs/validate';
|
||||
import { ProductStatus } from '../enums/base.enum';
|
||||
|
||||
export class VariationDTO extends Variation {}
|
||||
|
||||
export class WpProductDTO extends WpProduct {
|
||||
@ApiProperty({ description: '变体列表', type: VariationDTO, isArray: true })
|
||||
variations?: VariationDTO[];
|
||||
}
|
||||
|
||||
export class UpdateVariationDTO {
|
||||
@ApiProperty({ description: '产品名称' })
|
||||
@Rule(RuleType.string().optional())
|
||||
name?: string;
|
||||
|
||||
@ApiProperty({ description: 'SKU' })
|
||||
@Rule(RuleType.string().allow('').optional())
|
||||
sku?: string;
|
||||
|
||||
@ApiProperty({ description: '常规价格', type: Number })
|
||||
@Rule(RuleType.number().optional())
|
||||
regular_price?: number; // 常规价格
|
||||
|
||||
@ApiProperty({ description: '销售价格', type: Number })
|
||||
@Rule(RuleType.number().optional())
|
||||
sale_price?: number; // 销售价格
|
||||
|
||||
@ApiProperty({ description: '是否促销中', type: Boolean })
|
||||
@Rule(RuleType.boolean().optional())
|
||||
on_sale?: boolean; // 是否促销中
|
||||
}
|
||||
|
||||
export class UpdateWpProductDTO {
|
||||
@ApiProperty({ description: '变体名称' })
|
||||
@Rule(RuleType.string().optional())
|
||||
name?: string;
|
||||
|
||||
@ApiProperty({ description: 'SKU' })
|
||||
@Rule(RuleType.string().allow('').optional())
|
||||
sku?: string;
|
||||
|
||||
@ApiProperty({ description: '常规价格', type: Number })
|
||||
@Rule(RuleType.number().optional())
|
||||
regular_price?: number; // 常规价格
|
||||
|
||||
@ApiProperty({ description: '销售价格', type: Number })
|
||||
@Rule(RuleType.number().optional())
|
||||
sale_price?: number; // 销售价格
|
||||
|
||||
@ApiProperty({ description: '是否促销中', type: Boolean })
|
||||
@Rule(RuleType.boolean().optional())
|
||||
on_sale?: boolean; // 是否促销中
|
||||
|
||||
@ApiProperty({ description: '分类列表', type: [String] })
|
||||
@Rule(RuleType.array().items(RuleType.string()).optional())
|
||||
categories?: string[];
|
||||
|
||||
@ApiProperty({ description: '标签列表', type: [String] })
|
||||
@Rule(RuleType.array().items(RuleType.string()).optional())
|
||||
tags?: string[];
|
||||
|
||||
@ApiProperty({ description: '站点ID', required: false })
|
||||
@Rule(RuleType.number().optional())
|
||||
siteId?: number;
|
||||
}
|
||||
|
||||
export class QueryWpProductDTO {
|
||||
@ApiProperty({ example: '1', description: '页码' })
|
||||
@Rule(RuleType.number())
|
||||
current: number;
|
||||
|
||||
@ApiProperty({ example: '10', description: '每页大小' })
|
||||
@Rule(RuleType.number())
|
||||
pageSize: number;
|
||||
|
||||
@ApiProperty({ example: 'ZYN', description: '产品名' })
|
||||
@Rule(RuleType.string())
|
||||
name?: string;
|
||||
|
||||
@ApiProperty({ example: '1', description: '站点ID' })
|
||||
@Rule(RuleType.string())
|
||||
siteId?: string;
|
||||
|
||||
@ApiProperty({ description: '产品状态', enum: ProductStatus })
|
||||
@Rule(RuleType.string().valid(...Object.values(ProductStatus)))
|
||||
status?: ProductStatus;
|
||||
|
||||
@ApiProperty({ description: 'SKU列表', type: Array })
|
||||
@Rule(RuleType.array().items(RuleType.string()).single())
|
||||
skus?: string[];
|
||||
}
|
||||
|
||||
export class BatchSyncProductsDTO {
|
||||
@ApiProperty({ description: '产品ID列表', type: [Number] })
|
||||
@Rule(RuleType.array().items(RuleType.number()).required())
|
||||
productIds: number[];
|
||||
}
|
||||
|
||||
export class BatchUpdateTagsDTO {
|
||||
@ApiProperty({ description: '产品ID列表', type: [Number] })
|
||||
@Rule(RuleType.array().items(RuleType.number()).required())
|
||||
ids: number[];
|
||||
|
||||
@ApiProperty({ description: '标签列表', type: [String] })
|
||||
@Rule(RuleType.array().items(RuleType.string()).required())
|
||||
tags: string[];
|
||||
}
|
||||
|
||||
export class BatchUpdateProductsDTO {
|
||||
@ApiProperty({ description: '产品ID列表', type: [Number] })
|
||||
@Rule(RuleType.array().items(RuleType.number()).required())
|
||||
ids: number[];
|
||||
|
||||
@ApiProperty({ description: '常规价格', type: Number })
|
||||
@Rule(RuleType.number())
|
||||
regular_price?: number;
|
||||
|
||||
@ApiProperty({ description: '销售价格', type: Number })
|
||||
@Rule(RuleType.number())
|
||||
sale_price?: number;
|
||||
|
||||
@ApiProperty({ description: '分类列表', type: [String] })
|
||||
@Rule(RuleType.array().items(RuleType.string()))
|
||||
categories?: string[];
|
||||
|
||||
@ApiProperty({ description: '标签列表', type: [String] })
|
||||
@Rule(RuleType.array().items(RuleType.string()))
|
||||
tags?: string[];
|
||||
|
||||
@ApiProperty({ description: '状态', enum: ProductStatus })
|
||||
@Rule(RuleType.string().valid(...Object.values(ProductStatus)))
|
||||
status?: ProductStatus;
|
||||
}
|
||||
|
|
@ -1,13 +1,58 @@
|
|||
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
|
||||
import { Column, Entity, PrimaryGeneratedColumn, CreateDateColumn, UpdateDateColumn } from 'typeorm';
|
||||
|
||||
@Entity('customer')
|
||||
export class Customer {
|
||||
@PrimaryGeneratedColumn()
|
||||
id: number;
|
||||
|
||||
@Column({ nullable: true })
|
||||
site_id: number;
|
||||
|
||||
@Column({ nullable: true })
|
||||
origin_id: string;
|
||||
|
||||
@Column({ unique: true })
|
||||
email: string;
|
||||
|
||||
@Column({ nullable: true })
|
||||
first_name: string;
|
||||
|
||||
@Column({ nullable: true })
|
||||
last_name: string;
|
||||
|
||||
@Column({ nullable: true })
|
||||
fullname: string;
|
||||
|
||||
@Column({ nullable: true })
|
||||
username: string;
|
||||
|
||||
@Column({ nullable: true })
|
||||
phone: string;
|
||||
|
||||
@Column({ nullable: true })
|
||||
avatar: string;
|
||||
|
||||
@Column({ type: 'json', nullable: true })
|
||||
billing: any;
|
||||
|
||||
@Column({ type: 'json', nullable: true })
|
||||
shipping: any;
|
||||
|
||||
@Column({ type: 'json', nullable: true })
|
||||
raw: any;
|
||||
|
||||
@Column({ default: 0})
|
||||
rate: number;
|
||||
|
||||
@CreateDateColumn()
|
||||
created_at: Date;
|
||||
|
||||
@UpdateDateColumn()
|
||||
updated_at: Date;
|
||||
|
||||
@Column({ nullable: true })
|
||||
site_created_at: Date;
|
||||
|
||||
@Column({ nullable: true })
|
||||
site_updated_at: Date;
|
||||
}
|
||||
|
|
@ -9,10 +9,14 @@ import {
|
|||
PrimaryGeneratedColumn,
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
// import { Product } from './product.entity';
|
||||
|
||||
@Entity('order_sale')
|
||||
@Exclude()
|
||||
export class OrderSale {
|
||||
// @ManyToOne(() => Product, { onDelete: 'CASCADE' })
|
||||
// @JoinColumn({ name: 'productId' })
|
||||
// product: Product;
|
||||
@ApiProperty()
|
||||
@PrimaryGeneratedColumn()
|
||||
@Expose()
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ import { ProductStockComponent } from './product_stock_component.entity';
|
|||
import { ProductSiteSku } from './product_site_sku.entity';
|
||||
import { Category } from './category.entity';
|
||||
|
||||
@Entity()
|
||||
@Entity('product_v2')
|
||||
export class Product {
|
||||
@ApiProperty({
|
||||
example: '1',
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { Column, Entity, JoinTable, ManyToMany, PrimaryGeneratedColumn } from 't
|
|||
import { Area } from './area.entity';
|
||||
import { StockPoint } from './stock_point.entity';
|
||||
|
||||
@Entity('site')
|
||||
@Entity('site_v2')
|
||||
export class Site {
|
||||
@PrimaryGeneratedColumn()
|
||||
id: number;
|
||||
|
|
|
|||
|
|
@ -1,104 +0,0 @@
|
|||
import {
|
||||
Entity,
|
||||
Column,
|
||||
PrimaryGeneratedColumn,
|
||||
Unique,
|
||||
CreateDateColumn,
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
import { ApiProperty } from '@midwayjs/swagger';
|
||||
|
||||
@Entity('variation')
|
||||
@Unique(['siteId', 'externalProductId', 'externalVariationId']) // 确保变体的唯一性
|
||||
export class Variation {
|
||||
@ApiProperty({
|
||||
example: '1',
|
||||
description: 'ID',
|
||||
type: 'number',
|
||||
required: true,
|
||||
})
|
||||
@PrimaryGeneratedColumn()
|
||||
id: number;
|
||||
|
||||
@ApiProperty({
|
||||
description: '站点 id',
|
||||
example: 1,
|
||||
required: false
|
||||
})
|
||||
@Column({ nullable: true })
|
||||
siteId: number;
|
||||
|
||||
@ApiProperty({
|
||||
example: '1',
|
||||
description: 'wp产品ID',
|
||||
type: 'string',
|
||||
required: true,
|
||||
})
|
||||
@Column()
|
||||
externalProductId: string; // WooCommerce 产品 ID
|
||||
|
||||
@ApiProperty({
|
||||
example: '1',
|
||||
description: 'wp变体ID',
|
||||
type: 'string',
|
||||
required: true,
|
||||
})
|
||||
@Column()
|
||||
externalVariationId: string; // WooCommerce 变体 ID
|
||||
|
||||
@ApiProperty({
|
||||
example: '1',
|
||||
description: '对应WP产品表的ID',
|
||||
type: 'number',
|
||||
required: true,
|
||||
})
|
||||
@Column()
|
||||
productId: number; // 对应WP产品表的 ID
|
||||
|
||||
@ApiProperty({ description: 'sku', type: 'string' })
|
||||
@Column({ nullable: true })
|
||||
sku?: string; // sku 编码
|
||||
|
||||
@ApiProperty({
|
||||
description: '变体名称',
|
||||
type: 'string',
|
||||
required: true,
|
||||
})
|
||||
@Column()
|
||||
name: string;
|
||||
|
||||
@ApiProperty({ description: '常规价格', type: Number })
|
||||
@Column('decimal', { precision: 10, scale: 2, nullable: true })
|
||||
regular_price: number; // 常规价格
|
||||
|
||||
@ApiProperty({ description: '销售价格', type: Number })
|
||||
@Column('decimal', { precision: 10, scale: 2, nullable: true })
|
||||
sale_price: number; // 销售价格
|
||||
|
||||
@ApiProperty({ description: '是否促销中', type: Boolean })
|
||||
@Column({ nullable: true, type: Boolean })
|
||||
on_sale: boolean; // 是否促销中
|
||||
|
||||
@ApiProperty({ description: '是否删除', type: Boolean })
|
||||
@Column({ nullable: true, type: Boolean , default: false })
|
||||
on_delete: boolean; // 是否删除
|
||||
|
||||
@Column({ type: 'json', nullable: true })
|
||||
attributes: Record<string, any>; // 变体的属性
|
||||
|
||||
@ApiProperty({
|
||||
example: '2022-12-12 11:11:11',
|
||||
description: '创建时间',
|
||||
required: true,
|
||||
})
|
||||
@CreateDateColumn()
|
||||
createdAt: Date;
|
||||
|
||||
@ApiProperty({
|
||||
example: '2022-12-12 11:11:11',
|
||||
description: '更新时间',
|
||||
required: true,
|
||||
})
|
||||
@UpdateDateColumn()
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
|
@ -5,15 +5,6 @@ export interface IUserOptions {
|
|||
uid: number;
|
||||
}
|
||||
|
||||
export interface WpSite {
|
||||
id: string;
|
||||
wpApiUrl: string;
|
||||
consumerKey: string;
|
||||
consumerSecret: string;
|
||||
name: string;
|
||||
email: string;
|
||||
emailPswd: string;
|
||||
}
|
||||
|
||||
export interface PaginationParams {
|
||||
current?: number; // 当前页码
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import {
|
|||
CreateWebhookDTO,
|
||||
UpdateWebhookDTO,
|
||||
} from '../dto/site-api.dto';
|
||||
import { BatchOperationDTO, BatchOperationResultDTO } from '../dto/batch.dto';
|
||||
|
||||
export interface ISiteAdapter {
|
||||
/**
|
||||
|
|
@ -21,6 +22,11 @@ export interface ISiteAdapter {
|
|||
*/
|
||||
getProducts(params: UnifiedSearchParamsDTO): Promise<UnifiedPaginationDTO<UnifiedProductDTO>>;
|
||||
|
||||
/**
|
||||
* 获取所有产品
|
||||
*/
|
||||
getAllProducts(params?: UnifiedSearchParamsDTO): Promise<UnifiedProductDTO[]>;
|
||||
|
||||
/**
|
||||
* 获取单个产品
|
||||
*/
|
||||
|
|
@ -31,6 +37,11 @@ export interface ISiteAdapter {
|
|||
*/
|
||||
getOrders(params: UnifiedSearchParamsDTO): Promise<UnifiedPaginationDTO<UnifiedOrderDTO>>;
|
||||
|
||||
/**
|
||||
* 获取所有订单
|
||||
*/
|
||||
getAllOrders(params?: UnifiedSearchParamsDTO): Promise<UnifiedOrderDTO[]>;
|
||||
|
||||
/**
|
||||
* 获取单个订单
|
||||
*/
|
||||
|
|
@ -41,11 +52,21 @@ export interface ISiteAdapter {
|
|||
*/
|
||||
getSubscriptions(params: UnifiedSearchParamsDTO): Promise<UnifiedPaginationDTO<UnifiedSubscriptionDTO>>;
|
||||
|
||||
/**
|
||||
* 获取所有订阅
|
||||
*/
|
||||
getAllSubscriptions(params?: UnifiedSearchParamsDTO): Promise<UnifiedSubscriptionDTO[]>;
|
||||
|
||||
/**
|
||||
* 获取媒体列表
|
||||
*/
|
||||
getMedia(params: UnifiedSearchParamsDTO): Promise<UnifiedPaginationDTO<UnifiedMediaDTO>>;
|
||||
|
||||
/**
|
||||
* 获取所有媒体
|
||||
*/
|
||||
getAllMedia(params?: UnifiedSearchParamsDTO): Promise<UnifiedMediaDTO[]>;
|
||||
|
||||
/**
|
||||
* 创建媒体
|
||||
*/
|
||||
|
|
@ -56,6 +77,11 @@ export interface ISiteAdapter {
|
|||
*/
|
||||
getReviews(params: UnifiedSearchParamsDTO): Promise<UnifiedPaginationDTO<UnifiedReviewDTO>>;
|
||||
|
||||
/**
|
||||
* 获取所有评论
|
||||
*/
|
||||
getAllReviews(params?: UnifiedSearchParamsDTO): Promise<UnifiedReviewDTO[]>;
|
||||
|
||||
/**
|
||||
* 创建评论
|
||||
*/
|
||||
|
|
@ -101,27 +127,33 @@ export interface ISiteAdapter {
|
|||
*/
|
||||
deleteProduct(id: string | number): Promise<boolean>;
|
||||
|
||||
batchProcessProducts?(data: { create?: any[]; update?: any[]; delete?: Array<string | number> }): Promise<any>;
|
||||
batchProcessProducts?(data: BatchOperationDTO): Promise<BatchOperationResultDTO>;
|
||||
|
||||
createOrder(data: Partial<UnifiedOrderDTO>): Promise<UnifiedOrderDTO>;
|
||||
updateOrder(id: string | number, data: Partial<UnifiedOrderDTO>): Promise<boolean>;
|
||||
deleteOrder(id: string | number): Promise<boolean>;
|
||||
|
||||
batchProcessOrders?(data: { create?: any[]; update?: any[]; delete?: Array<string | number> }): Promise<any>;
|
||||
batchProcessOrders?(data: BatchOperationDTO): Promise<BatchOperationResultDTO>;
|
||||
|
||||
getCustomers(params: UnifiedSearchParamsDTO): Promise<UnifiedPaginationDTO<UnifiedCustomerDTO>>;
|
||||
getAllCustomers(params?: UnifiedSearchParamsDTO): Promise<UnifiedCustomerDTO[]>;
|
||||
getCustomer(id: string | number): Promise<UnifiedCustomerDTO>;
|
||||
createCustomer(data: Partial<UnifiedCustomerDTO>): Promise<UnifiedCustomerDTO>;
|
||||
updateCustomer(id: string | number, data: Partial<UnifiedCustomerDTO>): Promise<UnifiedCustomerDTO>;
|
||||
deleteCustomer(id: string | number): Promise<boolean>;
|
||||
|
||||
batchProcessCustomers?(data: { create?: any[]; update?: any[]; delete?: Array<string | number> }): Promise<any>;
|
||||
batchProcessCustomers?(data: BatchOperationDTO): Promise<BatchOperationResultDTO>;
|
||||
|
||||
/**
|
||||
* 获取webhooks列表
|
||||
*/
|
||||
getWebhooks(params: UnifiedSearchParamsDTO): Promise<UnifiedWebhookPaginationDTO>;
|
||||
|
||||
/**
|
||||
* 获取所有webhooks
|
||||
*/
|
||||
getAllWebhooks(params?: UnifiedSearchParamsDTO): Promise<UnifiedWebhookDTO[]>;
|
||||
|
||||
/**
|
||||
* 获取单个webhook
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -23,6 +23,13 @@ export class AuthMiddleware implements IMiddleware<Context, NextFunction> {
|
|||
'/webhook/woocommerce',
|
||||
'/logistics/getTrackingNumber',
|
||||
'/logistics/getListByTrackingId',
|
||||
'/product/categories/all',
|
||||
'/product/category/1/attributes',
|
||||
'/product/category/2/attributes',
|
||||
'/product/category/3/attributes',
|
||||
'/product/category/4/attributes',
|
||||
'/product/list',
|
||||
'/dict/items',
|
||||
];
|
||||
|
||||
match(ctx: Context) {
|
||||
|
|
|
|||
|
|
@ -1,9 +1,12 @@
|
|||
import { Provide } from '@midwayjs/core';
|
||||
import { Provide, Inject } from '@midwayjs/core';
|
||||
import { InjectEntityModel } from '@midwayjs/typeorm';
|
||||
import { Order } from '../entity/order.entity';
|
||||
import { Repository } from 'typeorm';
|
||||
import { CustomerTag } from '../entity/customer_tag.entity';
|
||||
import { Customer } from '../entity/customer.entity';
|
||||
import { SiteApiService } from './site-api.service';
|
||||
import { UnifiedCustomerDTO, UnifiedPaginationDTO, UnifiedSearchParamsDTO } from '../dto/site-api.dto';
|
||||
import { SyncOperationResult, BatchErrorItem } from '../dto/batch.dto';
|
||||
|
||||
@Provide()
|
||||
export class CustomerService {
|
||||
|
|
@ -16,7 +19,183 @@ export class CustomerService {
|
|||
@InjectEntityModel(Customer)
|
||||
customerModel: Repository<Customer>;
|
||||
|
||||
async getCustomerList(param: Record<string, any>) {
|
||||
@Inject()
|
||||
siteApiService: SiteApiService;
|
||||
|
||||
/**
|
||||
* 根据邮箱查找客户
|
||||
*/
|
||||
async findCustomerByEmail(email: string): Promise<Customer | null> {
|
||||
return await this.customerModel.findOne({ where: { email } });
|
||||
}
|
||||
|
||||
/**
|
||||
* 将站点客户数据映射为本地客户实体数据
|
||||
* 处理字段映射和数据转换,确保所有字段正确同步
|
||||
*/
|
||||
private mapSiteCustomerToCustomer(siteCustomer: UnifiedCustomerDTO, siteId: number): Partial<Customer> {
|
||||
return {
|
||||
site_id: siteId, // 使用站点ID而不是客户ID
|
||||
origin_id: "" + siteCustomer.id,
|
||||
email: siteCustomer.email,
|
||||
first_name: siteCustomer.first_name,
|
||||
last_name: siteCustomer.last_name,
|
||||
fullname: siteCustomer.fullname || `${siteCustomer.first_name || ''} ${siteCustomer.last_name || ''}`.trim(),
|
||||
username: siteCustomer.username || '',
|
||||
phone: siteCustomer.phone || '',
|
||||
avatar: siteCustomer.avatar,
|
||||
billing: siteCustomer.billing,
|
||||
shipping: siteCustomer.shipping,
|
||||
raw: siteCustomer.raw || siteCustomer,
|
||||
site_created_at: this.parseDate(siteCustomer.date_created),
|
||||
site_updated_at: this.parseDate(siteCustomer.date_modified)
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 解析日期字符串或时间戳
|
||||
*/
|
||||
private parseDate(dateValue: any): Date | null {
|
||||
if (!dateValue) return null;
|
||||
|
||||
if (dateValue instanceof Date) {
|
||||
return dateValue;
|
||||
}
|
||||
|
||||
if (typeof dateValue === 'number') {
|
||||
// 处理Unix时间戳(秒或毫秒)
|
||||
return new Date(dateValue > 9999999999 ? dateValue : dateValue * 1000);
|
||||
}
|
||||
|
||||
if (typeof dateValue === 'string') {
|
||||
const date = new Date(dateValue);
|
||||
return isNaN(date.getTime()) ? null : date;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建新客户
|
||||
*/
|
||||
async createCustomer(customerData: Partial<Customer>): Promise<Customer> {
|
||||
const customer = this.customerModel.create(customerData);
|
||||
return await this.customerModel.save(customer);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新客户信息
|
||||
*/
|
||||
async updateCustomer(id: number, customerData: Partial<Customer>): Promise<Customer> {
|
||||
await this.customerModel.update(id, customerData);
|
||||
return await this.customerModel.findOne({ where: { id } });
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建或更新客户(upsert)
|
||||
* 如果客户存在则更新,不存在则创建
|
||||
*/
|
||||
async upsertCustomer(
|
||||
customerData: Partial<Customer>,
|
||||
): Promise<{ customer: Customer; isCreated: boolean }> {
|
||||
if(!customerData.email) throw new Error("客户邮箱不能为空");
|
||||
// 首先尝试根据邮箱查找现有客户
|
||||
const existingCustomer = await this.findCustomerByEmail(customerData.email);
|
||||
|
||||
if (existingCustomer) {
|
||||
// 如果客户存在,更新客户信息
|
||||
const updatedCustomer = await this.updateCustomer(existingCustomer.id, customerData);
|
||||
return { customer: updatedCustomer, isCreated: false };
|
||||
} else {
|
||||
// 如果客户不存在,创建新客户
|
||||
const newCustomer = await this.createCustomer(customerData);
|
||||
return { customer: newCustomer, isCreated: true };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量创建或更新客户
|
||||
* 使用事务确保数据一致性
|
||||
*/
|
||||
async upsertManyCustomers(
|
||||
customersData: Array<Partial<Customer>>
|
||||
): Promise<{
|
||||
customers: Customer[];
|
||||
created: number;
|
||||
updated: number;
|
||||
processed: number;
|
||||
errors: BatchErrorItem[];
|
||||
}> {
|
||||
const results = {
|
||||
customers: [],
|
||||
created: 0,
|
||||
updated: 0,
|
||||
processed: 0,
|
||||
errors: []
|
||||
};
|
||||
|
||||
// 批量处理每个客户
|
||||
for (const customerData of customersData) {
|
||||
try {
|
||||
const result = await this.upsertCustomer(customerData);
|
||||
results.customers.push(result.customer);
|
||||
|
||||
if (result.isCreated) {
|
||||
results.created++;
|
||||
} else {
|
||||
results.updated++;
|
||||
}
|
||||
results.processed++;
|
||||
} catch (error) {
|
||||
// 记录错误但不中断整个批量操作
|
||||
results.errors.push({
|
||||
identifier: customerData.email || String(customerData.id) || 'unknown',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从站点同步客户数据
|
||||
* 第一步:调用adapter获取站点客户数据
|
||||
* 第二步:通过upsertManyCustomers保存这些客户
|
||||
*/
|
||||
async syncCustomersFromSite(
|
||||
siteId: number,
|
||||
params?: UnifiedSearchParamsDTO
|
||||
): Promise<SyncOperationResult> {
|
||||
try {
|
||||
// 第一步:获取适配器并从站点获取客户数据
|
||||
const adapter = await this.siteApiService.getAdapter(siteId);
|
||||
const siteCustomers = await adapter.getAllCustomers(params || {});
|
||||
|
||||
// 第二步:将站点客户数据转换为客户实体数据
|
||||
const customersData = siteCustomers.map(siteCustomer => {
|
||||
return this.mapSiteCustomerToCustomer(siteCustomer, siteId);
|
||||
});
|
||||
|
||||
// 第三步:批量upsert客户数据
|
||||
const upsertResult = await this.upsertManyCustomers(customersData);
|
||||
return {
|
||||
total: siteCustomers.length,
|
||||
processed: upsertResult.customers.length,
|
||||
synced: upsertResult.customers.length,
|
||||
updated: upsertResult.updated,
|
||||
created: upsertResult.created,
|
||||
errors: upsertResult.errors
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
// 如果获取适配器或站点数据失败,抛出错误
|
||||
throw new Error(`同步客户数据失败: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
async getCustomerStatisticList(param: Record<string, any>) {
|
||||
const {
|
||||
current = 1,
|
||||
pageSize = 10,
|
||||
|
|
@ -148,6 +327,112 @@ export class CustomerService {
|
|||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取纯粹的客户列表(不包含订单统计信息)
|
||||
* 支持基本的分页、搜索和排序功能
|
||||
* 使用TypeORM查询构建器实现
|
||||
*/
|
||||
async getCustomerList(param: Record<string, any>): Promise<UnifiedPaginationDTO<any>>{
|
||||
const {
|
||||
current = 1,
|
||||
pageSize = 10,
|
||||
email,
|
||||
firstName,
|
||||
lastName,
|
||||
phone,
|
||||
state,
|
||||
rate,
|
||||
sorterKey,
|
||||
sorterValue,
|
||||
} = param;
|
||||
|
||||
// 创建查询构建器
|
||||
const queryBuilder = this.customerModel
|
||||
.createQueryBuilder('c')
|
||||
.leftJoinAndSelect(
|
||||
'customer_tag',
|
||||
'ct',
|
||||
'ct.email = c.email'
|
||||
)
|
||||
.select([
|
||||
'c.id',
|
||||
'c.email',
|
||||
'c.first_name',
|
||||
'c.last_name',
|
||||
'c.fullname',
|
||||
'c.username',
|
||||
'c.phone',
|
||||
'c.avatar',
|
||||
'c.billing',
|
||||
'c.shipping',
|
||||
'c.rate',
|
||||
'c.site_id',
|
||||
'c.created_at',
|
||||
'c.updated_at',
|
||||
'c.site_created_at',
|
||||
'c.site_updated_at'
|
||||
])
|
||||
.groupBy('c.id');
|
||||
|
||||
// 邮箱搜索
|
||||
if (email) {
|
||||
queryBuilder.andWhere('c.email LIKE :email', { email: `%${email}%` });
|
||||
}
|
||||
|
||||
// 姓名搜索
|
||||
if (firstName) {
|
||||
queryBuilder.andWhere('c.first_name LIKE :firstName', { firstName: `%${firstName}%` });
|
||||
}
|
||||
|
||||
if (lastName) {
|
||||
queryBuilder.andWhere('c.last_name LIKE :lastName', { lastName: `%${lastName}%` });
|
||||
}
|
||||
|
||||
// 电话搜索
|
||||
if (phone) {
|
||||
queryBuilder.andWhere('c.phone LIKE :phone', { phone: `%${phone}%` });
|
||||
}
|
||||
|
||||
// 省份搜索
|
||||
if (state) {
|
||||
queryBuilder.andWhere("JSON_UNQUOTE(JSON_EXTRACT(c.billing, '$.state')) = :state", { state });
|
||||
}
|
||||
|
||||
// 评分过滤
|
||||
if (rate !== undefined && rate !== null) {
|
||||
queryBuilder.andWhere('c.rate = :rate', { rate: Number(rate) });
|
||||
}
|
||||
|
||||
// 排序处理
|
||||
if (sorterKey) {
|
||||
const order = sorterValue === 'descend' ? 'DESC' : 'ASC';
|
||||
queryBuilder.orderBy(`c.${sorterKey}`, order);
|
||||
} else {
|
||||
queryBuilder.orderBy('c.created_at', 'DESC');
|
||||
}
|
||||
|
||||
// 分页
|
||||
queryBuilder.skip((current - 1) * pageSize).take(pageSize);
|
||||
|
||||
// 执行查询
|
||||
const [items, total] = await queryBuilder.getManyAndCount();
|
||||
|
||||
// 处理tags字段,将逗号分隔的字符串转换为数组
|
||||
const processedItems = items.map(item => {
|
||||
const plainItem = JSON.parse(JSON.stringify(item));
|
||||
plainItem.tags = plainItem.tags ? plainItem.tags.split(',').filter(tag => tag) : [];
|
||||
return plainItem;
|
||||
});
|
||||
|
||||
return {
|
||||
items: processedItems,
|
||||
total,
|
||||
page: current,
|
||||
per_page: pageSize,
|
||||
totalPages: Math.ceil(total / pageSize),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
async addTag(email: string, tag: string) {
|
||||
const isExist = await this.customerTagModel.findOneBy({ email, tag });
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import { Inject, Provide } from '@midwayjs/core';
|
||||
import { WPService } from './wp.service';
|
||||
import { WpSite } from '../interface';
|
||||
import { Order } from '../entity/order.entity';
|
||||
import { In, Like, Repository } from 'typeorm';
|
||||
import { InjectEntityModel, TypeORMDataSourceManager } from '@midwayjs/typeorm';
|
||||
|
|
@ -8,7 +7,6 @@ import { plainToClass } from 'class-transformer';
|
|||
import { OrderItem } from '../entity/order_item.entity';
|
||||
|
||||
import { OrderSale } from '../entity/order_sale.entity';
|
||||
import { WpProduct } from '../entity/wp_product.entity';
|
||||
import { Product } from '../entity/product.entity';
|
||||
import { OrderFee } from '../entity/order_fee.entity';
|
||||
import { OrderRefund } from '../entity/order_refund.entity';
|
||||
|
|
@ -22,7 +20,6 @@ import {
|
|||
OrderStatus,
|
||||
StockRecordOperationType,
|
||||
} from '../enums/base.enum';
|
||||
import { Variation } from '../entity/variation.entity';
|
||||
import { CreateOrderNoteDTO, QueryOrderSalesDTO } from '../dto/order.dto';
|
||||
import dayjs = require('dayjs');
|
||||
import { OrderDetailRes } from '../dto/reponse.dto';
|
||||
|
|
@ -58,13 +55,6 @@ export class OrderService {
|
|||
@InjectEntityModel(OrderSale)
|
||||
orderSaleModel: Repository<OrderSale>;
|
||||
|
||||
|
||||
@InjectEntityModel(WpProduct)
|
||||
wpProductModel: Repository<WpProduct>;
|
||||
|
||||
@InjectEntityModel(Variation)
|
||||
variationModel: Repository<Variation>;
|
||||
|
||||
@InjectEntityModel(Product)
|
||||
productModel: Repository<Product>;
|
||||
|
||||
|
|
@ -1444,8 +1434,7 @@ export class OrderService {
|
|||
async cancelOrder(id: number) {
|
||||
const order = await this.orderModel.findOne({ where: { id } });
|
||||
if (!order) throw new Error(`订单 ${id}不存在`);
|
||||
const s: any = await this.siteService.get(Number(order.siteId), true);
|
||||
const site = { id: String(s.id), wpApiUrl: s.apiUrl, consumerKey: s.consumerKey, consumerSecret: s.consumerSecret, name: s.name, email: '', emailPswd: '' } as WpSite;
|
||||
const site = await this.siteService.get(Number(order.siteId), true);
|
||||
if (order.status !== OrderStatus.CANCEL) {
|
||||
await this.wpService.updateOrder(site, order.externalOrderId, {
|
||||
status: OrderStatus.CANCEL,
|
||||
|
|
@ -1620,86 +1609,84 @@ export class OrderService {
|
|||
//换货确认按钮改成调用这个方法
|
||||
//换货功能更新OrderSale和Orderitem数据
|
||||
async updateExchangeOrder(orderId: number, data: any) {
|
||||
try {
|
||||
const dataSource = this.dataSourceManager.getDataSource('default');
|
||||
let transactionError = undefined;
|
||||
throw new Error('暂未实现')
|
||||
// try {
|
||||
// const dataSource = this.dataSourceManager.getDataSource('default');
|
||||
// let transactionError = undefined;
|
||||
|
||||
await dataSource.transaction(async manager => {
|
||||
const orderRepo = manager.getRepository(Order);
|
||||
const orderSaleRepo = manager.getRepository(OrderSale);
|
||||
const orderItemRepo = manager.getRepository(OrderItem);
|
||||
// await dataSource.transaction(async manager => {
|
||||
// const orderRepo = manager.getRepository(Order);
|
||||
// const orderSaleRepo = manager.getRepository(OrderSale);
|
||||
// const orderItemRepo = manager.getRepository(OrderItem);
|
||||
|
||||
|
||||
const productRepo = manager.getRepository(Product);
|
||||
const WpProductRepo = manager.getRepository(WpProduct);
|
||||
// const productRepo = manager.getRepository(ProductV2);
|
||||
|
||||
const order = await orderRepo.findOneBy({ id: orderId });
|
||||
let product: Product;
|
||||
let wpProduct: WpProduct;
|
||||
// const order = await orderRepo.findOneBy({ id: orderId });
|
||||
// let product: ProductV2;
|
||||
|
||||
await orderSaleRepo.delete({ orderId });
|
||||
await orderItemRepo.delete({ orderId });
|
||||
for (const sale of data['sales']) {
|
||||
product = await productRepo.findOneBy({ sku: sale['sku'] });
|
||||
await orderSaleRepo.save({
|
||||
orderId,
|
||||
siteId: order.siteId,
|
||||
productId: product.id,
|
||||
name: product.name,
|
||||
sku: sale['sku'],
|
||||
quantity: sale['quantity'],
|
||||
});
|
||||
};
|
||||
// await orderSaleRepo.delete({ orderId });
|
||||
// await orderItemRepo.delete({ orderId });
|
||||
// for (const sale of data['sales']) {
|
||||
// product = await productRepo.findOneBy({ sku: sale['sku'] });
|
||||
// await orderSaleRepo.save({
|
||||
// orderId,
|
||||
// siteId: order.siteId,
|
||||
// productId: product.id,
|
||||
// name: product.name,
|
||||
// sku: sale['sku'],
|
||||
// quantity: sale['quantity'],
|
||||
// });
|
||||
// };
|
||||
|
||||
for (const item of data['items']) {
|
||||
wpProduct = await WpProductRepo.findOneBy({ sku: item['sku'] });
|
||||
// for (const item of data['items']) {
|
||||
// product = await productRepo.findOneBy({ sku: item['sku'] });
|
||||
|
||||
// await orderItemRepo.save({
|
||||
// orderId,
|
||||
// siteId: order.siteId,
|
||||
// productId: product.id,
|
||||
// name: product.name,
|
||||
// externalOrderId: order.externalOrderId,
|
||||
// externalProductId: product.externalProductId,
|
||||
|
||||
await orderItemRepo.save({
|
||||
orderId,
|
||||
siteId: order.siteId,
|
||||
productId: wpProduct.id,
|
||||
name: wpProduct.name,
|
||||
externalOrderId: order.externalOrderId,
|
||||
externalProductId: wpProduct.externalProductId,
|
||||
// sku: item['sku'],
|
||||
// quantity: item['quantity'],
|
||||
// });
|
||||
|
||||
sku: item['sku'],
|
||||
quantity: item['quantity'],
|
||||
});
|
||||
// };
|
||||
|
||||
};
|
||||
// //将是否换货状态改为true
|
||||
// await orderRepo.update(
|
||||
// order.id
|
||||
// , {
|
||||
// is_exchange: true
|
||||
// });
|
||||
|
||||
//将是否换货状态改为true
|
||||
await orderRepo.update(
|
||||
order.id
|
||||
, {
|
||||
is_exchange: true
|
||||
});
|
||||
// //查询这个用户换过多少次货
|
||||
// const counts = await orderRepo.countBy({
|
||||
// is_editable: true,
|
||||
// customer_email: order.customer_email,
|
||||
// });
|
||||
|
||||
//查询这个用户换过多少次货
|
||||
const counts = await orderRepo.countBy({
|
||||
is_editable: true,
|
||||
customer_email: order.customer_email,
|
||||
});
|
||||
// //批量更新当前用户换货次数
|
||||
// await orderRepo.update({
|
||||
// customer_email: order.customer_email
|
||||
// }, {
|
||||
// exchange_frequency: counts
|
||||
// });
|
||||
|
||||
//批量更新当前用户换货次数
|
||||
await orderRepo.update({
|
||||
customer_email: order.customer_email
|
||||
}, {
|
||||
exchange_frequency: counts
|
||||
});
|
||||
// }).catch(error => {
|
||||
// transactionError = error;
|
||||
// });
|
||||
|
||||
}).catch(error => {
|
||||
transactionError = error;
|
||||
});
|
||||
|
||||
if (transactionError !== undefined) {
|
||||
throw new Error(`更新物流信息错误:${transactionError.message}`);
|
||||
}
|
||||
return true;
|
||||
} catch (error) {
|
||||
throw new Error(`更新发货产品失败:${error.message}`);
|
||||
}
|
||||
// if (transactionError !== undefined) {
|
||||
// throw new Error(`更新物流信息错误:${transactionError.message}`);
|
||||
// }
|
||||
// return true;
|
||||
// } catch (error) {
|
||||
// throw new Error(`更新发货产品失败:${error.message}`);
|
||||
// }
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,8 +19,6 @@ import {
|
|||
SizePaginatedResponse,
|
||||
} from '../dto/reponse.dto';
|
||||
import { InjectEntityModel } from '@midwayjs/typeorm';
|
||||
import { WpProduct } from '../entity/wp_product.entity';
|
||||
import { Variation } from '../entity/variation.entity';
|
||||
import { Dict } from '../entity/dict.entity';
|
||||
import { DictItem } from '../entity/dict_item.entity';
|
||||
import { Context } from '@midwayjs/koa';
|
||||
|
|
@ -53,12 +51,6 @@ export class ProductService {
|
|||
@InjectEntityModel(DictItem)
|
||||
dictItemModel: Repository<DictItem>;
|
||||
|
||||
@InjectEntityModel(WpProduct)
|
||||
wpProductModel: Repository<WpProduct>;
|
||||
|
||||
@InjectEntityModel(Variation)
|
||||
variationModel: Repository<Variation>;
|
||||
|
||||
@InjectEntityModel(Stock)
|
||||
stockModel: Repository<Stock>;
|
||||
|
||||
|
|
@ -74,11 +66,6 @@ export class ProductService {
|
|||
@InjectEntityModel(Category)
|
||||
categoryModel: Repository<Category>;
|
||||
|
||||
|
||||
// 获取所有 WordPress 商品
|
||||
async getWpProducts() {
|
||||
return this.wpProductModel.find();
|
||||
}
|
||||
// 获取所有分类
|
||||
async getCategoriesAll(): Promise<Category[]> {
|
||||
return this.categoryModel.find({
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import { SiteService } from './site.service';
|
|||
import { Site } from '../entity/site.entity';
|
||||
import { UnifiedReviewDTO } from '../dto/site-api.dto';
|
||||
import { ShopyyReview } from '../dto/shopyy.dto';
|
||||
import { BatchOperationDTO, BatchOperationResultDTO } from '../dto/batch.dto';
|
||||
|
||||
/**
|
||||
* ShopYY平台服务实现
|
||||
|
|
@ -533,10 +534,40 @@ export class ShopyyService {
|
|||
* @param data 批量操作数据
|
||||
* @returns 处理结果
|
||||
*/
|
||||
async batchProcessProducts(site: any, data: { create?: any[]; update?: any[]; delete?: any[] }): Promise<any> {
|
||||
async batchProcessProducts(site: any, data: BatchOperationDTO): Promise<BatchOperationResultDTO> {
|
||||
// ShopYY API: POST /products/batch
|
||||
const response = await this.request(site, 'products/batch', 'POST', data);
|
||||
return response.data;
|
||||
const result = response.data;
|
||||
|
||||
// 转换 ShopYY 批量操作结果为统一格式
|
||||
const errors: Array<{identifier: string, error: string}> = [];
|
||||
|
||||
// 假设 ShopYY 返回格式与 WooCommerce 类似: { create: [...], update: [...], delete: [...] }
|
||||
// 错误信息可能在每个项目的 error 字段中
|
||||
const checkForErrors = (items: any[]) => {
|
||||
items.forEach(item => {
|
||||
if (item.error) {
|
||||
errors.push({
|
||||
identifier: String(item.id || item.sku || 'unknown'),
|
||||
error: typeof item.error === 'string' ? item.error : JSON.stringify(item.error)
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 检查每个操作类型的结果中的错误
|
||||
if (result.create) checkForErrors(result.create);
|
||||
if (result.update) checkForErrors(result.update);
|
||||
if (result.delete) checkForErrors(result.delete);
|
||||
|
||||
return {
|
||||
total: (data.create?.length || 0) + (data.update?.length || 0) + (data.delete?.length || 0),
|
||||
processed: (result.create?.length || 0) + (result.update?.length || 0) + (result.delete?.length || 0),
|
||||
created: result.create?.length || 0,
|
||||
updated: result.update?.length || 0,
|
||||
deleted: result.delete?.length || 0,
|
||||
errors: errors
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ import { Provide, Scope, ScopeEnum } from '@midwayjs/core';
|
|||
import { InjectEntityModel } from '@midwayjs/typeorm';
|
||||
import { Repository, Like, In } from 'typeorm';
|
||||
import { Site } from '../entity/site.entity';
|
||||
import { WpSite } from '../interface';
|
||||
import { CreateSiteDTO, UpdateSiteDTO } from '../dto/site.dto';
|
||||
import { Area } from '../entity/area.entity';
|
||||
import { StockPoint } from '../entity/stock_point.entity';
|
||||
|
|
@ -19,29 +18,6 @@ export class SiteService {
|
|||
@InjectEntityModel(StockPoint)
|
||||
stockPointModel: Repository<StockPoint>;
|
||||
|
||||
async syncFromConfig(sites: WpSite[] = []) {
|
||||
// 将配置中的 WpSite 同步到数据库 Site 表(用于一次性导入或初始化)
|
||||
for (const siteConfig of sites) {
|
||||
// 按站点名称查询是否已存在记录
|
||||
const exist = await this.siteModel.findOne({
|
||||
where: { name: siteConfig.name },
|
||||
});
|
||||
// 将 WpSite 字段映射为 Site 实体字段
|
||||
const payload: Partial<Site> = {
|
||||
name: siteConfig.name,
|
||||
apiUrl: (siteConfig as any).wpApiUrl,
|
||||
consumerKey: (siteConfig as any).consumerKey,
|
||||
consumerSecret: (siteConfig as any).consumerSecret,
|
||||
type: 'woocommerce',
|
||||
};
|
||||
// 存在则更新,不存在则插入新记录
|
||||
if (exist) {
|
||||
await this.siteModel.update({ id: exist.id }, payload);
|
||||
} else {
|
||||
await this.siteModel.insert(payload as Site);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async create(data: CreateSiteDTO) {
|
||||
// 从 DTO 中分离出区域代码和其他站点数据
|
||||
|
|
|
|||
|
|
@ -5,22 +5,22 @@
|
|||
import { Inject, Provide } from '@midwayjs/core';
|
||||
import axios, { AxiosRequestConfig } from 'axios';
|
||||
import WooCommerceRestApi, { WooCommerceRestApiVersion } from '@woocommerce/woocommerce-rest-api';
|
||||
import { WpProduct } from '../entity/wp_product.entity';
|
||||
import { Variation } from '../entity/variation.entity';
|
||||
import { UpdateVariationDTO, UpdateWpProductDTO } from '../dto/wp_product.dto';
|
||||
import { SiteService } from './site.service';
|
||||
import { IPlatformService } from '../interface/platform.interface';
|
||||
import { BatchOperationDTO, BatchOperationResultDTO } from '../dto/batch.dto';
|
||||
import * as FormData from 'form-data';
|
||||
import * as fs from 'fs';
|
||||
|
||||
import { WooProduct, WooVariation } from '../dto/woocommerce.dto';
|
||||
const MAX_PAGE_SIZE = 100;
|
||||
@Provide()
|
||||
export class WPService implements IPlatformService {
|
||||
getCustomer(site: any, id: number): Promise<any> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
@Inject()
|
||||
private readonly siteService: SiteService;
|
||||
|
||||
getCustomer(site: any, id: number): Promise<any> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
/**
|
||||
* 构建 URL,自动规范各段的斜杠,避免出现多 / 或少 / 导致请求失败
|
||||
* 使用示例:this.buildURL(wpApiUrl, '/wp-json', 'wc/v3/products', productId)
|
||||
|
|
@ -79,11 +79,79 @@ export class WPService implements IPlatformService {
|
|||
|
||||
/**
|
||||
* 通过 SDK 聚合分页数据,返回全部数据
|
||||
* 使用并发方式获取所有分页数据,提高性能
|
||||
* 默认按 date_created 倒序排列,确保获取最新的数据
|
||||
*/
|
||||
private async sdkGetAll<T>(api: WooCommerceRestApi, resource: string, params: Record<string, any> = {}, maxPages: number = 50): Promise<T[]> {
|
||||
// 直接传入较大的per_page参数,一次性获取所有数据
|
||||
const { items } = await this.sdkGetPage<T>(api, resource, { ...params, per_page: 100 });
|
||||
return items;
|
||||
async sdkGetAll<T>(api: WooCommerceRestApi, resource: string, params: Record<string, any> = {}, maxPages: number = MAX_PAGE_SIZE): Promise<T[]> {
|
||||
return this.sdkGetAllConcurrent<T>(api, resource, params, maxPages);
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过 SDK 聚合分页数据,使用并发方式获取所有分页数据
|
||||
* 支持自定义并发数和最大页数限制
|
||||
* 默认按 date_created 倒序排列,确保获取最新的数据
|
||||
*/
|
||||
private async sdkGetAllConcurrent<T>(
|
||||
api: WooCommerceRestApi,
|
||||
resource: string,
|
||||
params: Record<string, any> = {},
|
||||
maxPages: number = MAX_PAGE_SIZE,
|
||||
concurrencyLimit: number = 5
|
||||
): Promise<T[]> {
|
||||
// 设置默认排序为 date_created 倒序,确保获取最新数据
|
||||
const defaultParams = {
|
||||
order: 'desc', // 倒序,优先获取最新数据
|
||||
per_page: MAX_PAGE_SIZE,
|
||||
...params
|
||||
};
|
||||
|
||||
// 首先获取第一页数据,同时获取总页数信息
|
||||
const firstPage = await this.sdkGetPage<T>(api, resource, { ...defaultParams, page: 1 });
|
||||
const { items: firstPageItems, totalPages } = firstPage;
|
||||
|
||||
// 如果只有一页数据,直接返回
|
||||
if (totalPages <= 1) {
|
||||
return firstPageItems;
|
||||
}
|
||||
|
||||
// 限制最大页数,避免过多的并发请求
|
||||
const actualMaxPages = Math.min(totalPages, maxPages);
|
||||
|
||||
// 收集所有页面数据,从第二页开始
|
||||
const allItems = [...firstPageItems];
|
||||
let currentPage = 2;
|
||||
|
||||
// 使用并发限制,避免一次性发起过多请求
|
||||
while (currentPage <= actualMaxPages) {
|
||||
const batchPromises: Promise<T[]>[] = [];
|
||||
const batchSize = Math.min(concurrencyLimit, actualMaxPages - currentPage + 1);
|
||||
|
||||
// 创建当前批次的并发请求
|
||||
for (let i = 0; i < batchSize; i++) {
|
||||
const page = currentPage + i;
|
||||
const pagePromise = this.sdkGetPage<T>(api, resource, { ...defaultParams, page })
|
||||
.then(pageResult => pageResult.items)
|
||||
.catch(error => {
|
||||
console.error(`获取第 ${page} 页数据失败:`, error);
|
||||
return []; // 如果某页获取失败,返回空数组,不影响整体结果
|
||||
});
|
||||
|
||||
batchPromises.push(pagePromise);
|
||||
}
|
||||
|
||||
// 等待当前批次完成
|
||||
const batchResults = await Promise.all(batchPromises);
|
||||
|
||||
// 合并当前批次的数据
|
||||
for (const pageItems of batchResults) {
|
||||
allItems.push(...pageItems);
|
||||
}
|
||||
|
||||
// 移动到下一批次
|
||||
currentPage += batchSize;
|
||||
}
|
||||
|
||||
return allItems;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -174,7 +242,7 @@ export class WPService implements IPlatformService {
|
|||
|
||||
async getProducts(site: any, page: number = 1, pageSize: number = 100): Promise<any> {
|
||||
const api = this.createApi(site, 'wc/v3');
|
||||
return await this.sdkGetPage<WpProduct>(api, 'products', { page, per_page: pageSize });
|
||||
return await this.sdkGetPage<WooProduct>(api, 'products', { page, per_page: pageSize });
|
||||
}
|
||||
|
||||
async getProduct(site: any, id: number): Promise<any> {
|
||||
|
|
@ -195,17 +263,17 @@ export class WPService implements IPlatformService {
|
|||
|
||||
async getVariations(site: any, productId: number, page: number = 1, pageSize: number = 100): Promise<any> {
|
||||
const api = this.createApi(site, 'wc/v3');
|
||||
return await this.sdkGetPage<Variation>(api, `products/${productId}/variations`, { page, per_page: pageSize });
|
||||
return await this.sdkGetPage<WooVariation>(api, `products/${productId}/variations`, { page, per_page: pageSize });
|
||||
}
|
||||
|
||||
async getVariation(
|
||||
site: any,
|
||||
productId: number,
|
||||
variationId: number
|
||||
): Promise<Variation> {
|
||||
): Promise<WooVariation> {
|
||||
const api = this.createApi(site, 'wc/v3');
|
||||
const res = await api.get(`products/${productId}/variations/${variationId}`);
|
||||
return res.data as Variation;
|
||||
return res.data as WooVariation;
|
||||
}
|
||||
|
||||
async getOrder(
|
||||
|
|
@ -323,7 +391,7 @@ export class WPService implements IPlatformService {
|
|||
async updateProduct(
|
||||
site: any,
|
||||
productId: string,
|
||||
data: UpdateWpProductDTO
|
||||
data: WooProduct
|
||||
): Promise<any> {
|
||||
const { regular_price, sale_price, ...params } = data;
|
||||
const api = this.createApi(site, 'wc/v3');
|
||||
|
|
@ -440,7 +508,7 @@ export class WPService implements IPlatformService {
|
|||
site: any,
|
||||
productId: string,
|
||||
variationId: string,
|
||||
data: Partial<UpdateVariationDTO>
|
||||
data: Partial<WooVariation & any>
|
||||
): Promise<boolean> {
|
||||
const { regular_price, sale_price, ...params } = data;
|
||||
const api = this.createApi(site, 'wc/v3');
|
||||
|
|
@ -551,12 +619,42 @@ export class WPService implements IPlatformService {
|
|||
*/
|
||||
async batchProcessProducts(
|
||||
site: any,
|
||||
data: { create?: any[]; update?: any[]; delete?: any[] }
|
||||
): Promise<any> {
|
||||
data: BatchOperationDTO
|
||||
): Promise<BatchOperationResultDTO> {
|
||||
const api = this.createApi(site, 'wc/v3');
|
||||
try {
|
||||
const response = await api.post('products/batch', data);
|
||||
return response.data;
|
||||
const result = response.data;
|
||||
|
||||
// 转换 WooCommerce 批量操作结果为统一格式
|
||||
const errors: Array<{identifier: string, error: string}> = [];
|
||||
|
||||
// WooCommerce 返回格式: { create: [...], update: [...], delete: [...] }
|
||||
// 错误信息可能在每个项目的 error 字段中
|
||||
const checkForErrors = (items: any[]) => {
|
||||
items.forEach(item => {
|
||||
if (item.error) {
|
||||
errors.push({
|
||||
identifier: String(item.id || item.sku || 'unknown'),
|
||||
error: typeof item.error === 'string' ? item.error : JSON.stringify(item.error)
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 检查每个操作类型的结果中的错误
|
||||
if (result.create) checkForErrors(result.create);
|
||||
if (result.update) checkForErrors(result.update);
|
||||
if (result.delete) checkForErrors(result.delete);
|
||||
|
||||
return {
|
||||
total: (data.create?.length || 0) + (data.update?.length || 0) + (data.delete?.length || 0),
|
||||
processed: (result.create?.length || 0) + (result.update?.length || 0) + (result.delete?.length || 0),
|
||||
created: result.create?.length || 0,
|
||||
updated: result.update?.length || 0,
|
||||
deleted: result.delete?.length || 0,
|
||||
errors: errors
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('批量处理产品失败:', error.response?.data || error.message);
|
||||
throw error;
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -1 +1 @@
|
|||
{"root":["./src/configuration.ts","./src/interface.ts","./src/config/config.default.ts","./src/config/config.local.ts","./src/config/config.unittest.ts","./src/controller/api.controller.ts","./src/controller/area.controller.ts","./src/controller/customer.controller.ts","./src/controller/dict.controller.ts","./src/controller/locale.controller.ts","./src/controller/logistics.controller.ts","./src/controller/order.controller.ts","./src/controller/product.controller.ts","./src/controller/site.controller.ts","./src/controller/statistics.controller.ts","./src/controller/stock.controller.ts","./src/controller/subscription.controller.ts","./src/controller/template.controller.ts","./src/controller/user.controller.ts","./src/controller/webhook.controller.ts","./src/controller/wp_product.controller.ts","./src/db/datasource.ts","./src/db/migrations/1764238434984-product-dict-item-many-to-many.ts","./src/db/migrations/1764294088896-area.ts","./src/db/migrations/1764299629279-productstock.ts","./src/db/seeds/area.seeder.ts","./src/db/seeds/dict.seeder.ts","./src/db/seeds/template.seeder.ts","./src/decorator/user.decorator.ts","./src/dto/area.dto.ts","./src/dto/customer.dto.ts","./src/dto/dict.dto.ts","./src/dto/freightcom.dto.ts","./src/dto/logistics.dto.ts","./src/dto/order.dto.ts","./src/dto/product.dto.ts","./src/dto/reponse.dto.ts","./src/dto/site.dto.ts","./src/dto/statistics.dto.ts","./src/dto/stock.dto.ts","./src/dto/subscription.dto.ts","./src/dto/template.dto.ts","./src/dto/user.dto.ts","./src/dto/wp_product.dto.ts","./src/entity/area.entity.ts","./src/entity/auth_code.ts","./src/entity/customer.entity.ts","./src/entity/customer_tag.entity.ts","./src/entity/device_whitelist.ts","./src/entity/dict.entity.ts","./src/entity/dict_item.entity.ts","./src/entity/order.entity.ts","./src/entity/order_coupon.entity.ts","./src/entity/order_fee.entity.ts","./src/entity/order_item.entity.ts","./src/entity/order_item_original.entity.ts","./src/entity/order_items_original.entity.ts","./src/entity/order_note.entity.ts","./src/entity/order_refund.entity.ts","./src/entity/order_refund_item.entity.ts","./src/entity/order_sale.entity.ts","./src/entity/order_shipment.entity.ts","./src/entity/order_shipping.entity.ts","./src/entity/product.entity.ts","./src/entity/product_stock_component.entity.ts","./src/entity/purchase_order.entity.ts","./src/entity/purchase_order_item.entity.ts","./src/entity/service.entity.ts","./src/entity/shipment.entity.ts","./src/entity/shipment_item.entity.ts","./src/entity/shipping_address.entity.ts","./src/entity/site.entity.ts","./src/entity/stock.entity.ts","./src/entity/stock_point.entity.ts","./src/entity/stock_record.entity.ts","./src/entity/subscription.entity.ts","./src/entity/template.entity.ts","./src/entity/transfer.entity.ts","./src/entity/transfer_item.entity.ts","./src/entity/user.entity.ts","./src/entity/variation.entity.ts","./src/entity/wp_product.entity.ts","./src/enums/base.enum.ts","./src/filter/default.filter.ts","./src/filter/notfound.filter.ts","./src/job/sync_products.job.ts","./src/job/sync_shipment.job.ts","./src/middleware/auth.middleware.ts","./src/middleware/report.middleware.ts","./src/service/area.service.ts","./src/service/authcode.service.ts","./src/service/canadapost.service.ts","./src/service/customer.service.ts","./src/service/devicewhitelist.service.ts","./src/service/dict.service.ts","./src/service/freightcom.service.ts","./src/service/logistics.service.ts","./src/service/mail.service.ts","./src/service/order.service.ts","./src/service/product.service.ts","./src/service/site.service.ts","./src/service/statistics.service.ts","./src/service/stock.service.ts","./src/service/subscription.service.ts","./src/service/template.service.ts","./src/service/uni_express.service.ts","./src/service/user.service.ts","./src/service/wp.service.ts","./src/service/wp_product.service.ts","./src/utils/helper.util.ts","./src/utils/object-transform.util.ts","./src/utils/paginate.util.ts","./src/utils/paginated-response.util.ts","./src/utils/response-wrapper.util.ts","./src/utils/response.util.ts"],"version":"5.9.3"}
|
||||
{"root":["./src/configuration.ts","./src/interface.ts","./src/config/config.default.ts","./src/config/config.local.ts","./src/config/config.unittest.ts","./src/controller/api.controller.ts","./src/controller/area.controller.ts","./src/controller/customer.controller.ts","./src/controller/dict.controller.ts","./src/controller/locale.controller.ts","./src/controller/logistics.controller.ts","./src/controller/order.controller.ts","./src/controller/product.controller.ts","./src/controller/site.controller.ts","./src/controller/statistics.controller.ts","./src/controller/stock.controller.ts","./src/controller/subscription.controller.ts","./src/controller/template.controller.ts","./src/controller/user.controller.ts","./src/controller/webhook.controller.ts","./src/controller/wp_product.controller.ts","./src/db/datasource.ts","./src/db/migrations/1764238434984-product-dict-item-many-to-many.ts","./src/db/migrations/1764294088896-area.ts","./src/db/migrations/1764299629279-productstock.ts","./src/db/seeds/area.seeder.ts","./src/db/seeds/dict.seeder.ts","./src/db/seeds/template.seeder.ts","./src/decorator/user.decorator.ts","./src/dto/area.dto.ts","./src/dto/customer.dto.ts","./src/dto/dict.dto.ts","./src/dto/freightcom.dto.ts","./src/dto/logistics.dto.ts","./src/dto/order.dto.ts","./src/dto/product.dto.ts","./src/dto/reponse.dto.ts","./src/dto/site.dto.ts","./src/dto/statistics.dto.ts","./src/dto/stock.dto.ts","./src/dto/subscription.dto.ts","./src/dto/template.dto.ts","./src/dto/user.dto.ts","./src/dto/wp_product.dto.ts","./src/entity/area.entity.ts","./src/entity/auth_code.ts","./src/entity/customer.entity.ts","./src/entity/customer_tag.entity.ts","./src/entity/device_whitelist.ts","./src/entity/dict.entity.ts","./src/entity/dict_item.entity.ts","./src/entity/order.entity.ts","./src/entity/order_coupon.entity.ts","./src/entity/order_fee.entity.ts","./src/entity/order_item.entity.ts","./src/entity/order_item_original.entity.ts","./src/entity/order_items_original.entity.ts","./src/entity/order_note.entity.ts","./src/entity/order_refund.entity.ts","./src/entity/order_refund_item.entity.ts","./src/entity/order_sale.entity.ts","./src/entity/order_shipment.entity.ts","./src/entity/order_shipping.entity.ts","./src/entity/product.ts","./src/entity/product_stock_component.entity.ts","./src/entity/purchase_order.entity.ts","./src/entity/purchase_order_item.entity.ts","./src/entity/service.entity.ts","./src/entity/shipment.entity.ts","./src/entity/shipment_item.entity.ts","./src/entity/shipping_address.entity.ts","./src/entity/site.entity.ts","./src/entity/stock.entity.ts","./src/entity/stock_point.entity.ts","./src/entity/stock_record.entity.ts","./src/entity/subscription.entity.ts","./src/entity/template.entity.ts","./src/entity/transfer.entity.ts","./src/entity/transfer_item.entity.ts","./src/entity/user.entity.ts","./src/entity/variation.entity.ts","./src/entity/wp_product.ts","./src/enums/base.enum.ts","./src/filter/default.filter.ts","./src/filter/notfound.filter.ts","./src/job/sync_products.job.ts","./src/job/sync_shipment.job.ts","./src/middleware/auth.middleware.ts","./src/middleware/report.middleware.ts","./src/service/area.service.ts","./src/service/authcode.service.ts","./src/service/canadapost.service.ts","./src/service/customer.service.ts","./src/service/devicewhitelist.service.ts","./src/service/dict.service.ts","./src/service/freightcom.service.ts","./src/service/logistics.service.ts","./src/service/mail.service.ts","./src/service/order.service.ts","./src/service/product.service.ts","./src/service/site.service.ts","./src/service/statistics.service.ts","./src/service/stock.service.ts","./src/service/subscription.service.ts","./src/service/template.service.ts","./src/service/uni_express.service.ts","./src/service/user.service.ts","./src/service/wp.service.ts","./src/service/wp_product.service.ts","./src/utils/helper.util.ts","./src/utils/object-transform.util.ts","./src/utils/paginate.util.ts","./src/utils/paginated-response.util.ts","./src/utils/response-wrapper.util.ts","./src/utils/response.util.ts"],"version":"5.9.3"}
|
||||
Loading…
Reference in New Issue
????????