feat(customer): 实现客户数据同步功能并增强客户管理
重构客户服务层,添加客户数据同步功能 扩展客户实体字段以支持完整客户信息存储 优化客户列表查询性能并添加统计功能 移除废弃的WpSite相关代码和配置
This commit is contained in:
parent
a02758a926
commit
8e7ec2372d
|
|
@ -752,7 +752,6 @@ export class WooCommerceAdapter implements ISiteAdapter {
|
||||||
raw: item,
|
raw: item,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async getCustomers(params: UnifiedSearchParamsDTO): Promise<UnifiedPaginationDTO<UnifiedCustomerDTO>> {
|
async getCustomers(params: UnifiedSearchParamsDTO): Promise<UnifiedPaginationDTO<UnifiedCustomerDTO>> {
|
||||||
const requestParams = this.mapCustomerSearchParams(params);
|
const requestParams = this.mapCustomerSearchParams(params);
|
||||||
const { items, total, totalPages, page, per_page } = await this.wpService.fetchResourcePaged<any>(
|
const { items, total, totalPages, page, per_page } = await this.wpService.fetchResourcePaged<any>(
|
||||||
|
|
@ -794,3 +793,4 @@ export class WooCommerceAdapter implements ISiteAdapter {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -116,17 +116,6 @@ export default {
|
||||||
// secret: 'YOONE2024!@abc',
|
// secret: 'YOONE2024!@abc',
|
||||||
// expiresIn: '7d',
|
// expiresIn: '7d',
|
||||||
// },
|
// },
|
||||||
// wpSite: [
|
|
||||||
// {
|
|
||||||
// id: '2',
|
|
||||||
// wpApiUrl: 'http://localhost:10004',
|
|
||||||
// consumerKey: 'ck_dc9e151e9048c8ed3e27f35ac79d2bf7d6840652',
|
|
||||||
// consumerSecret: 'cs_d05d625d7b0ac05c6d765671d8417f41d9477e38',
|
|
||||||
// name: 'Local',
|
|
||||||
// email: 'tom@yoonevape.com',
|
|
||||||
// emailPswd: '',
|
|
||||||
// },
|
|
||||||
// ],
|
|
||||||
swagger: {
|
swagger: {
|
||||||
auth: {
|
auth: {
|
||||||
name: 'authorization',
|
name: 'authorization',
|
||||||
|
|
|
||||||
|
|
@ -16,8 +16,10 @@ export default {
|
||||||
dataSource: {
|
dataSource: {
|
||||||
default: {
|
default: {
|
||||||
host: 'localhost',
|
host: 'localhost',
|
||||||
|
port: "23306",
|
||||||
username: 'root',
|
username: 'root',
|
||||||
password: '12345678',
|
password: '12345678',
|
||||||
|
database: 'inventory',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
@ -25,7 +27,7 @@ export default {
|
||||||
origin: '*', // 允许所有来源跨域请求
|
origin: '*', // 允许所有来源跨域请求
|
||||||
allowMethods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'], // 允许的 HTTP 方法
|
allowMethods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'], // 允许的 HTTP 方法
|
||||||
allowHeaders: ['Content-Type', 'Authorization'], // 允许的自定义请求头
|
allowHeaders: ['Content-Type', 'Authorization'], // 允许的自定义请求头
|
||||||
credentials: true, // 允许携带凭据(cookies等)
|
credentials: true, // 允许携带凭据(cookies等)
|
||||||
},
|
},
|
||||||
jwt: {
|
jwt: {
|
||||||
secret: 'YOONE2024!@abc',
|
secret: 'YOONE2024!@abc',
|
||||||
|
|
@ -33,34 +35,38 @@ export default {
|
||||||
},
|
},
|
||||||
wpSite: [
|
wpSite: [
|
||||||
{
|
{
|
||||||
id: '-1',
|
id: '200',
|
||||||
siteName: 'Admin',
|
wpApiUrl: "http://simple.local",
|
||||||
email: '2469687281@qq.com',
|
consumerKey: 'ck_11b446d0dfd221853830b782049cf9a17553f886',
|
||||||
},
|
consumerSecret: 'cs_2b06729269f659dcef675b8cdff542bf3c1da7e8',
|
||||||
{
|
name: 'LocalSimple',
|
||||||
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',
|
email: '2469687281@qq.com',
|
||||||
emailPswd: 'lulin91.',
|
emailPswd: 'lulin91.',
|
||||||
},
|
},
|
||||||
// {
|
// {
|
||||||
// id: '2',
|
// id: '2',
|
||||||
|
// wpApiUrl: 'http://t2-shop.local/',
|
||||||
|
// consumerKey: 'ck_a369473a6451dbaec63d19cbfd74a074b2c5f742',
|
||||||
|
// consumerSecret: 'cs_0946bbbeea1bfefff08a69e817ac62a48412df8c',
|
||||||
|
// name: 'Local',
|
||||||
|
// email: '2469687281@qq.com',
|
||||||
|
// emailPswd: 'lulin91.',
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// id: '3',
|
||||||
|
// wpApiUrl: 'http://t1-shop.local/',
|
||||||
|
// consumerKey: 'ck_a369473a6451dbaec63d19cbfd74a074b2c5f742',
|
||||||
|
// consumerSecret: 'cs_0946bbbeea1bfefff08a69e817ac62a48412df8c',
|
||||||
|
// name: 'Local-test-2',
|
||||||
|
// email: '2469687281@qq.com',
|
||||||
|
// emailPswd: 'lulin91.',
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// id: '2',
|
||||||
// wpApiUrl: 'http://localhost:10004',
|
// wpApiUrl: 'http://localhost:10004',
|
||||||
// consumerKey: 'ck_dc9e151e9048c8ed3e27f35ac79d2bf7d6840652',
|
// consumerKey: 'ck_dc9e151e9048c8ed3e27f35ac79d2bf7d6840652',
|
||||||
// consumerSecret: 'cs_d05d625d7b0ac05c6d765671d8417f41d9477e38',
|
// consumerSecret: 'cs_d05d625d7b0ac05c6d765671d8417f41d9477e38',
|
||||||
// siteName: 'Local',
|
// name: 'Local',
|
||||||
// email: 'tom@yoonevape.com',
|
// email: 'tom@yoonevape.com',
|
||||||
// emailPswd: 'lulin91.',
|
// emailPswd: 'lulin91.',
|
||||||
// },
|
// },
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import { successResponse, errorResponse } from '../utils/response.util';
|
||||||
import { CustomerService } from '../service/customer.service';
|
import { CustomerService } from '../service/customer.service';
|
||||||
import { QueryCustomerListDTO, CustomerTagDTO } from '../dto/customer.dto';
|
import { QueryCustomerListDTO, CustomerTagDTO } from '../dto/customer.dto';
|
||||||
import { ApiOkResponse } from '@midwayjs/swagger';
|
import { ApiOkResponse } from '@midwayjs/swagger';
|
||||||
|
import { UnifiedSearchParamsDTO } from '../dto/site-api.dto';
|
||||||
|
|
||||||
@Controller('/customer')
|
@Controller('/customer')
|
||||||
export class CustomerController {
|
export class CustomerController {
|
||||||
|
|
@ -13,7 +14,18 @@ export class CustomerController {
|
||||||
@Get('/getcustomerlist')
|
@Get('/getcustomerlist')
|
||||||
async getCustomerList(@Query() query: QueryCustomerListDTO) {
|
async getCustomerList(@Query() query: QueryCustomerListDTO) {
|
||||||
try {
|
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);
|
return successResponse(result);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return errorResponse(error.message);
|
return errorResponse(error.message);
|
||||||
|
|
@ -63,4 +75,24 @@ export class CustomerController {
|
||||||
return errorResponse(error.message);
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -23,6 +23,7 @@ import {
|
||||||
CancelShipOrderDTO,
|
CancelShipOrderDTO,
|
||||||
BatchShipOrdersDTO,
|
BatchShipOrdersDTO,
|
||||||
} from '../dto/site-api.dto';
|
} from '../dto/site-api.dto';
|
||||||
|
import { BatchOperationDTO, BatchOperationResultDTO } from '../dto/batch.dto';
|
||||||
import { SiteApiService } from '../service/site-api.service';
|
import { SiteApiService } from '../service/site-api.service';
|
||||||
import { errorResponse, successResponse } from '../utils/response.util';
|
import { errorResponse, successResponse } from '../utils/response.util';
|
||||||
import { ILogger } from '@midwayjs/core';
|
import { ILogger } from '@midwayjs/core';
|
||||||
|
|
@ -533,10 +534,10 @@ export class SiteApiController {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post('/:siteId/products/batch')
|
@Post('/:siteId/products/batch')
|
||||||
@ApiOkResponse({ type: Object })
|
@ApiOkResponse({ type: BatchOperationResultDTO })
|
||||||
async batchProducts(
|
async batchProducts(
|
||||||
@Param('siteId') siteId: number,
|
@Param('siteId') siteId: number,
|
||||||
@Body() body: { create?: any[]; update?: any[]; delete?: Array<string | number> }
|
@Body() body: BatchOperationDTO
|
||||||
) {
|
) {
|
||||||
this.logger.info(`[Site API] 批量处理产品开始, siteId: ${siteId}`);
|
this.logger.info(`[Site API] 批量处理产品开始, siteId: ${siteId}`);
|
||||||
try {
|
try {
|
||||||
|
|
@ -549,14 +550,18 @@ export class SiteApiController {
|
||||||
const created: any[] = [];
|
const created: any[] = [];
|
||||||
const updated: any[] = [];
|
const updated: any[] = [];
|
||||||
const deleted: Array<string | number> = [];
|
const deleted: Array<string | number> = [];
|
||||||
const failed: any[] = [];
|
const errors: Array<{identifier: string, error: string}> = [];
|
||||||
|
|
||||||
if (body.create?.length) {
|
if (body.create?.length) {
|
||||||
for (const item of body.create) {
|
for (const item of body.create) {
|
||||||
try {
|
try {
|
||||||
const data = await adapter.createProduct(item);
|
const data = await adapter.createProduct(item);
|
||||||
created.push(data);
|
created.push(data);
|
||||||
} catch (e) {
|
} 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);
|
const data = await adapter.updateProduct(id, item);
|
||||||
updated.push(data);
|
updated.push(data);
|
||||||
} catch (e) {
|
} 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 {
|
try {
|
||||||
const ok = await adapter.deleteProduct(id);
|
const ok = await adapter.deleteProduct(id);
|
||||||
if (ok) deleted.push(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) {
|
} 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}`);
|
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) {
|
} catch (error) {
|
||||||
this.logger.error(`[Site API] 批量处理产品失败, siteId: ${siteId}, 错误信息: ${error.message}`);
|
this.logger.error(`[Site API] 批量处理产品失败, siteId: ${siteId}, 错误信息: ${error.message}`);
|
||||||
return errorResponse(error.message);
|
return errorResponse(error.message);
|
||||||
|
|
@ -789,10 +811,10 @@ export class SiteApiController {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post('/:siteId/orders/batch')
|
@Post('/:siteId/orders/batch')
|
||||||
@ApiOkResponse({ type: Object })
|
@ApiOkResponse({ type: BatchOperationResultDTO })
|
||||||
async batchOrders(
|
async batchOrders(
|
||||||
@Param('siteId') siteId: number,
|
@Param('siteId') siteId: number,
|
||||||
@Body() body: { create?: any[]; update?: any[]; delete?: Array<string | number> }
|
@Body() body: BatchOperationDTO
|
||||||
) {
|
) {
|
||||||
this.logger.info(`[Site API] 批量处理订单开始, siteId: ${siteId}`);
|
this.logger.info(`[Site API] 批量处理订单开始, siteId: ${siteId}`);
|
||||||
try {
|
try {
|
||||||
|
|
@ -800,14 +822,18 @@ export class SiteApiController {
|
||||||
const created: any[] = [];
|
const created: any[] = [];
|
||||||
const updated: any[] = [];
|
const updated: any[] = [];
|
||||||
const deleted: Array<string | number> = [];
|
const deleted: Array<string | number> = [];
|
||||||
const failed: any[] = [];
|
const errors: Array<{identifier: string, error: string}> = [];
|
||||||
|
|
||||||
if (body.create?.length) {
|
if (body.create?.length) {
|
||||||
for (const item of body.create) {
|
for (const item of body.create) {
|
||||||
try {
|
try {
|
||||||
const data = await adapter.createOrder(item);
|
const data = await adapter.createOrder(item);
|
||||||
created.push(data);
|
created.push(data);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
failed.push({ action: 'create', item, error: (e as any).message });
|
errors.push({
|
||||||
|
identifier: String(item.id || item.order_number || 'unknown'),
|
||||||
|
error: (e as any).message
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -817,9 +843,15 @@ export class SiteApiController {
|
||||||
const id = item.id;
|
const id = item.id;
|
||||||
const ok = await adapter.updateOrder(id, item);
|
const ok = await adapter.updateOrder(id, item);
|
||||||
if (ok) updated.push(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) {
|
} catch (e) {
|
||||||
failed.push({ action: 'update', item, error: (e as any).message });
|
errors.push({
|
||||||
|
identifier: String(item.id || 'unknown'),
|
||||||
|
error: (e as any).message
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -828,14 +860,28 @@ export class SiteApiController {
|
||||||
try {
|
try {
|
||||||
const ok = await adapter.deleteOrder(id);
|
const ok = await adapter.deleteOrder(id);
|
||||||
if (ok) deleted.push(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) {
|
} 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}`);
|
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) {
|
} catch (error) {
|
||||||
this.logger.error(`[Site API] 批量处理订单失败, siteId: ${siteId}, 错误信息: ${error.message}`);
|
this.logger.error(`[Site API] 批量处理订单失败, siteId: ${siteId}, 错误信息: ${error.message}`);
|
||||||
return errorResponse(error.message);
|
return errorResponse(error.message);
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { Body, Controller, Get, Inject, Param, Put, Post, Query } from '@midwayjs/core';
|
import { Body, Controller, Get, Inject, Param, Put, Post, Query } from '@midwayjs/core';
|
||||||
import { ApiOkResponse } from '@midwayjs/swagger';
|
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 { errorResponse, successResponse } from '../utils/response.util';
|
||||||
import { SiteService } from '../service/site.service';
|
import { SiteService } from '../service/site.service';
|
||||||
import { CreateSiteDTO, DisableSiteDTO, QuerySiteDTO, UpdateSiteDTO } from '../dto/site.dto';
|
import { CreateSiteDTO, DisableSiteDTO, QuerySiteDTO, UpdateSiteDTO } from '../dto/site.dto';
|
||||||
|
|
@ -10,7 +10,7 @@ export class SiteController {
|
||||||
@Inject()
|
@Inject()
|
||||||
siteService: SiteService;
|
siteService: SiteService;
|
||||||
|
|
||||||
@ApiOkResponse({ description: '关联网站', type: WpSitesResponse })
|
@ApiOkResponse({ description: '关联网站', type: SitesResponse })
|
||||||
@Get('/all')
|
@Get('/all')
|
||||||
async all() {
|
async all() {
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { HttpStatus, Inject } from '@midwayjs/core';
|
import { HttpStatus, ILogger, Inject, Logger } from '@midwayjs/core';
|
||||||
import {
|
import {
|
||||||
Controller,
|
Controller,
|
||||||
Post,
|
Post,
|
||||||
|
|
@ -25,6 +25,9 @@ export class WebhookController {
|
||||||
@Inject()
|
@Inject()
|
||||||
ctx: Context;
|
ctx: Context;
|
||||||
|
|
||||||
|
@Logger()
|
||||||
|
logger: ILogger;
|
||||||
|
|
||||||
@Inject()
|
@Inject()
|
||||||
private readonly siteService: SiteService;
|
private readonly siteService: SiteService;
|
||||||
|
|
||||||
|
|
@ -48,7 +51,7 @@ export class WebhookController {
|
||||||
// 从数据库获取站点配置
|
// 从数据库获取站点配置
|
||||||
const site = await this.siteService.get(siteId, true);
|
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');
|
console.log('domain not match');
|
||||||
return {
|
return {
|
||||||
code: HttpStatus.BAD_REQUEST,
|
code: HttpStatus.BAD_REQUEST,
|
||||||
|
|
|
||||||
|
|
@ -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,27 @@ export class CustomerTagDTO {
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
tag: string;
|
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;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -25,7 +25,7 @@ import { Dict } from '../entity/dict.entity';
|
||||||
|
|
||||||
export class BooleanRes extends SuccessWrapper(Boolean) {}
|
export class BooleanRes extends SuccessWrapper(Boolean) {}
|
||||||
//网站配置返回数据
|
//网站配置返回数据
|
||||||
export class WpSitesResponse extends SuccessArrayWrapper(SiteConfig) {}
|
export class SitesResponse extends SuccessArrayWrapper(SiteConfig) {}
|
||||||
//产品分页数据
|
//产品分页数据
|
||||||
export class ProductPaginatedResponse extends PaginatedWrapper(Product) {}
|
export class ProductPaginatedResponse extends PaginatedWrapper(Product) {}
|
||||||
//产品分页返回数据
|
//产品分页返回数据
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,58 @@
|
||||||
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
|
import { Column, Entity, PrimaryGeneratedColumn, CreateDateColumn, UpdateDateColumn } from 'typeorm';
|
||||||
|
|
||||||
@Entity('customer')
|
@Entity('customer')
|
||||||
export class Customer {
|
export class Customer {
|
||||||
@PrimaryGeneratedColumn()
|
@PrimaryGeneratedColumn()
|
||||||
id: number;
|
id: number;
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
site_id: number;
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
origin_id: string;
|
||||||
|
|
||||||
@Column({ unique: true })
|
@Column({ unique: true })
|
||||||
email: string;
|
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})
|
@Column({ default: 0})
|
||||||
rate: number;
|
rate: number;
|
||||||
|
|
||||||
|
@CreateDateColumn()
|
||||||
|
created_at: Date;
|
||||||
|
|
||||||
|
@UpdateDateColumn()
|
||||||
|
updated_at: Date;
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
site_created_at: Date;
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
site_updated_at: Date;
|
||||||
}
|
}
|
||||||
|
|
@ -5,15 +5,6 @@ export interface IUserOptions {
|
||||||
uid: number;
|
uid: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface WpSite {
|
|
||||||
id: string;
|
|
||||||
wpApiUrl: string;
|
|
||||||
consumerKey: string;
|
|
||||||
consumerSecret: string;
|
|
||||||
name: string;
|
|
||||||
email: string;
|
|
||||||
emailPswd: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface PaginationParams {
|
export interface PaginationParams {
|
||||||
current?: number; // 当前页码
|
current?: number; // 当前页码
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ import {
|
||||||
CreateWebhookDTO,
|
CreateWebhookDTO,
|
||||||
UpdateWebhookDTO,
|
UpdateWebhookDTO,
|
||||||
} from '../dto/site-api.dto';
|
} from '../dto/site-api.dto';
|
||||||
|
import { BatchOperationDTO, BatchOperationResultDTO } from '../dto/batch.dto';
|
||||||
|
|
||||||
export interface ISiteAdapter {
|
export interface ISiteAdapter {
|
||||||
/**
|
/**
|
||||||
|
|
@ -101,13 +102,13 @@ export interface ISiteAdapter {
|
||||||
*/
|
*/
|
||||||
deleteProduct(id: string | number): Promise<boolean>;
|
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>;
|
createOrder(data: Partial<UnifiedOrderDTO>): Promise<UnifiedOrderDTO>;
|
||||||
updateOrder(id: string | number, data: Partial<UnifiedOrderDTO>): Promise<boolean>;
|
updateOrder(id: string | number, data: Partial<UnifiedOrderDTO>): Promise<boolean>;
|
||||||
deleteOrder(id: string | number): 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>>;
|
getCustomers(params: UnifiedSearchParamsDTO): Promise<UnifiedPaginationDTO<UnifiedCustomerDTO>>;
|
||||||
getCustomer(id: string | number): Promise<UnifiedCustomerDTO>;
|
getCustomer(id: string | number): Promise<UnifiedCustomerDTO>;
|
||||||
|
|
@ -115,7 +116,7 @@ export interface ISiteAdapter {
|
||||||
updateCustomer(id: string | number, data: Partial<UnifiedCustomerDTO>): Promise<UnifiedCustomerDTO>;
|
updateCustomer(id: string | number, data: Partial<UnifiedCustomerDTO>): Promise<UnifiedCustomerDTO>;
|
||||||
deleteCustomer(id: string | number): Promise<boolean>;
|
deleteCustomer(id: string | number): Promise<boolean>;
|
||||||
|
|
||||||
batchProcessCustomers?(data: { create?: any[]; update?: any[]; delete?: Array<string | number> }): Promise<any>;
|
batchProcessCustomers?(data: BatchOperationDTO): Promise<BatchOperationResultDTO>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取webhooks列表
|
* 获取webhooks列表
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,13 @@ export class AuthMiddleware implements IMiddleware<Context, NextFunction> {
|
||||||
'/webhook/woocommerce',
|
'/webhook/woocommerce',
|
||||||
'/logistics/getTrackingNumber',
|
'/logistics/getTrackingNumber',
|
||||||
'/logistics/getListByTrackingId',
|
'/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) {
|
match(ctx: Context) {
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,12 @@
|
||||||
import { Provide } from '@midwayjs/core';
|
import { Provide, Inject } from '@midwayjs/core';
|
||||||
import { InjectEntityModel } from '@midwayjs/typeorm';
|
import { InjectEntityModel } from '@midwayjs/typeorm';
|
||||||
import { Order } from '../entity/order.entity';
|
import { Order } from '../entity/order.entity';
|
||||||
import { Repository } from 'typeorm';
|
import { Repository } from 'typeorm';
|
||||||
import { CustomerTag } from '../entity/customer_tag.entity';
|
import { CustomerTag } from '../entity/customer_tag.entity';
|
||||||
import { Customer } from '../entity/customer.entity';
|
import { Customer } from '../entity/customer.entity';
|
||||||
|
import { SiteApiService } from './site-api.service';
|
||||||
|
import { UnifiedCustomerDTO, UnifiedSearchParamsDTO } from '../dto/site-api.dto';
|
||||||
|
import { SyncOperationResult, BatchErrorItem } from '../dto/batch.dto';
|
||||||
|
|
||||||
@Provide()
|
@Provide()
|
||||||
export class CustomerService {
|
export class CustomerService {
|
||||||
|
|
@ -16,7 +19,183 @@ export class CustomerService {
|
||||||
@InjectEntityModel(Customer)
|
@InjectEntityModel(Customer)
|
||||||
customerModel: Repository<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 siteCustomersResult = await adapter.getCustomers(params || {});
|
||||||
|
|
||||||
|
// 第二步:将站点客户数据转换为客户实体数据
|
||||||
|
const customersData = siteCustomersResult.items.map(siteCustomer => {
|
||||||
|
return this.mapSiteCustomerToCustomer(siteCustomer, siteId);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 第三步:批量upsert客户数据
|
||||||
|
const upsertResult = await this.upsertManyCustomers(customersData);
|
||||||
|
return {
|
||||||
|
total: siteCustomersResult.total,
|
||||||
|
processed: upsertResult.customers.length,
|
||||||
|
synced: upsertResult.customers.length,
|
||||||
|
updated: upsertResult.updated,
|
||||||
|
created: upsertResult.created,
|
||||||
|
errors: upsertResult.errors
|
||||||
|
};
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
// 如果获取适配器或站点数据失败,抛出错误
|
||||||
|
throw new Error(`同步客户数据失败: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getCustomerStatisticList(param: Record<string, any>) {
|
||||||
const {
|
const {
|
||||||
current = 1,
|
current = 1,
|
||||||
pageSize = 10,
|
pageSize = 10,
|
||||||
|
|
@ -148,6 +327,112 @@ export class CustomerService {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取纯粹的客户列表(不包含订单统计信息)
|
||||||
|
* 支持基本的分页、搜索和排序功能
|
||||||
|
* 使用TypeORM查询构建器实现
|
||||||
|
*/
|
||||||
|
async getCustomerList(param: Record<string, any>): Promise<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',
|
||||||
|
'GROUP_CONCAT(ct.tag) as tags'
|
||||||
|
])
|
||||||
|
.groupBy('c.id');
|
||||||
|
|
||||||
|
// 邮箱搜索
|
||||||
|
if (email) {
|
||||||
|
queryBuilder.andWhere('c.email LIKE :email', { email: `%${email}%` });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 姓名搜索
|
||||||
|
if (firstName) {
|
||||||
|
queryBuilder.andWhere('c.first_name LIKE :firstName', { firstName: `%${firstName}%` });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lastName) {
|
||||||
|
queryBuilder.andWhere('c.last_name LIKE :lastName', { lastName: `%${lastName}%` });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 电话搜索
|
||||||
|
if (phone) {
|
||||||
|
queryBuilder.andWhere('c.phone LIKE :phone', { phone: `%${phone}%` });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 省份搜索
|
||||||
|
if (state) {
|
||||||
|
queryBuilder.andWhere("JSON_UNQUOTE(JSON_EXTRACT(c.billing, '$.state')) = :state", { state });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 评分过滤
|
||||||
|
if (rate !== undefined && rate !== null) {
|
||||||
|
queryBuilder.andWhere('c.rate = :rate', { rate: Number(rate) });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 排序处理
|
||||||
|
if (sorterKey) {
|
||||||
|
const order = sorterValue === 'descend' ? 'DESC' : 'ASC';
|
||||||
|
queryBuilder.orderBy(`c.${sorterKey}`, order);
|
||||||
|
} else {
|
||||||
|
queryBuilder.orderBy('c.created_at', 'DESC');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分页
|
||||||
|
queryBuilder.skip((current - 1) * pageSize).take(pageSize);
|
||||||
|
|
||||||
|
// 执行查询
|
||||||
|
const [items, total] = await queryBuilder.getManyAndCount();
|
||||||
|
|
||||||
|
// 处理tags字段,将逗号分隔的字符串转换为数组
|
||||||
|
const processedItems = items.map(item => {
|
||||||
|
const plainItem = JSON.parse(JSON.stringify(item));
|
||||||
|
plainItem.tags = plainItem.tags ? plainItem.tags.split(',').filter(tag => tag) : [];
|
||||||
|
return plainItem;
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
items: processedItems,
|
||||||
|
total,
|
||||||
|
current,
|
||||||
|
pageSize,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
async addTag(email: string, tag: string) {
|
async addTag(email: string, tag: string) {
|
||||||
const isExist = await this.customerTagModel.findOneBy({ email, tag });
|
const isExist = await this.customerTagModel.findOneBy({ email, tag });
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import { Inject, Provide } from '@midwayjs/core';
|
import { Inject, Provide } from '@midwayjs/core';
|
||||||
import { WPService } from './wp.service';
|
import { WPService } from './wp.service';
|
||||||
import { WpSite } from '../interface';
|
|
||||||
import { Order } from '../entity/order.entity';
|
import { Order } from '../entity/order.entity';
|
||||||
import { In, Like, Repository } from 'typeorm';
|
import { In, Like, Repository } from 'typeorm';
|
||||||
import { InjectEntityModel, TypeORMDataSourceManager } from '@midwayjs/typeorm';
|
import { InjectEntityModel, TypeORMDataSourceManager } from '@midwayjs/typeorm';
|
||||||
|
|
@ -1447,8 +1446,7 @@ export class OrderService {
|
||||||
async cancelOrder(id: number) {
|
async cancelOrder(id: number) {
|
||||||
const order = await this.orderModel.findOne({ where: { id } });
|
const order = await this.orderModel.findOne({ where: { id } });
|
||||||
if (!order) throw new Error(`订单 ${id}不存在`);
|
if (!order) throw new Error(`订单 ${id}不存在`);
|
||||||
const s: any = await this.siteService.get(Number(order.siteId), true);
|
const site = 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;
|
|
||||||
if (order.status !== OrderStatus.CANCEL) {
|
if (order.status !== OrderStatus.CANCEL) {
|
||||||
await this.wpService.updateOrder(site, order.externalOrderId, {
|
await this.wpService.updateOrder(site, order.externalOrderId, {
|
||||||
status: OrderStatus.CANCEL,
|
status: OrderStatus.CANCEL,
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import { SiteService } from './site.service';
|
||||||
import { Site } from '../entity/site.entity';
|
import { Site } from '../entity/site.entity';
|
||||||
import { UnifiedReviewDTO } from '../dto/site-api.dto';
|
import { UnifiedReviewDTO } from '../dto/site-api.dto';
|
||||||
import { ShopyyReview } from '../dto/shopyy.dto';
|
import { ShopyyReview } from '../dto/shopyy.dto';
|
||||||
|
import { BatchOperationDTO, BatchOperationResultDTO } from '../dto/batch.dto';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ShopYY平台服务实现
|
* ShopYY平台服务实现
|
||||||
|
|
@ -533,10 +534,40 @@ export class ShopyyService {
|
||||||
* @param data 批量操作数据
|
* @param data 批量操作数据
|
||||||
* @returns 处理结果
|
* @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
|
// ShopYY API: POST /products/batch
|
||||||
const response = await this.request(site, 'products/batch', 'POST', data);
|
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 { InjectEntityModel } from '@midwayjs/typeorm';
|
||||||
import { Repository, Like, In } from 'typeorm';
|
import { Repository, Like, In } from 'typeorm';
|
||||||
import { Site } from '../entity/site.entity';
|
import { Site } from '../entity/site.entity';
|
||||||
import { WpSite } from '../interface';
|
|
||||||
import { CreateSiteDTO, UpdateSiteDTO } from '../dto/site.dto';
|
import { CreateSiteDTO, UpdateSiteDTO } from '../dto/site.dto';
|
||||||
import { Area } from '../entity/area.entity';
|
import { Area } from '../entity/area.entity';
|
||||||
import { StockPoint } from '../entity/stock_point.entity';
|
import { StockPoint } from '../entity/stock_point.entity';
|
||||||
|
|
@ -19,29 +18,6 @@ export class SiteService {
|
||||||
@InjectEntityModel(StockPoint)
|
@InjectEntityModel(StockPoint)
|
||||||
stockPointModel: Repository<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) {
|
async create(data: CreateSiteDTO) {
|
||||||
// 从 DTO 中分离出区域代码和其他站点数据
|
// 从 DTO 中分离出区域代码和其他站点数据
|
||||||
|
|
|
||||||
|
|
@ -10,9 +10,10 @@ import { Variation } from '../entity/variation.entity';
|
||||||
import { UpdateVariationDTO, UpdateWpProductDTO } from '../dto/wp_product.dto';
|
import { UpdateVariationDTO, UpdateWpProductDTO } from '../dto/wp_product.dto';
|
||||||
import { SiteService } from './site.service';
|
import { SiteService } from './site.service';
|
||||||
import { IPlatformService } from '../interface/platform.interface';
|
import { IPlatformService } from '../interface/platform.interface';
|
||||||
|
import { BatchOperationDTO, BatchOperationResultDTO } from '../dto/batch.dto';
|
||||||
import * as FormData from 'form-data';
|
import * as FormData from 'form-data';
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
|
const MAX_PAGE_SIZE = 100;
|
||||||
@Provide()
|
@Provide()
|
||||||
export class WPService implements IPlatformService {
|
export class WPService implements IPlatformService {
|
||||||
getCustomer(site: any, id: number): Promise<any> {
|
getCustomer(site: any, id: number): Promise<any> {
|
||||||
|
|
@ -79,11 +80,80 @@ export class WPService implements IPlatformService {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 通过 SDK 聚合分页数据,返回全部数据
|
* 通过 SDK 聚合分页数据,返回全部数据
|
||||||
|
* 使用并发方式获取所有分页数据,提高性能
|
||||||
|
* 默认按 date_created 倒序排列,确保获取最新的数据
|
||||||
*/
|
*/
|
||||||
private async sdkGetAll<T>(api: WooCommerceRestApi, resource: string, params: Record<string, any> = {}, maxPages: number = 50): Promise<T[]> {
|
private async sdkGetAll<T>(api: WooCommerceRestApi, resource: string, params: Record<string, any> = {}, maxPages: number = MAX_PAGE_SIZE): Promise<T[]> {
|
||||||
// 直接传入较大的per_page参数,一次性获取所有数据
|
return this.sdkGetAllConcurrent<T>(api, resource, params, maxPages);
|
||||||
const { items } = await this.sdkGetPage<T>(api, resource, { ...params, per_page: 100 });
|
}
|
||||||
return items;
|
|
||||||
|
/**
|
||||||
|
* 通过 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 = {
|
||||||
|
orderby: 'date_created',
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -551,12 +621,42 @@ export class WPService implements IPlatformService {
|
||||||
*/
|
*/
|
||||||
async batchProcessProducts(
|
async batchProcessProducts(
|
||||||
site: any,
|
site: any,
|
||||||
data: { create?: any[]; update?: any[]; delete?: any[] }
|
data: BatchOperationDTO
|
||||||
): Promise<any> {
|
): Promise<BatchOperationResultDTO> {
|
||||||
const api = this.createApi(site, 'wc/v3');
|
const api = this.createApi(site, 'wc/v3');
|
||||||
try {
|
try {
|
||||||
const response = await api.post('products/batch', data);
|
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) {
|
} catch (error) {
|
||||||
console.error('批量处理产品失败:', error.response?.data || error.message);
|
console.error('批量处理产品失败:', error.response?.data || error.message);
|
||||||
throw error;
|
throw error;
|
||||||
|
|
|
||||||
|
|
@ -555,7 +555,7 @@ export class WpProductService {
|
||||||
// 同步一个网站
|
// 同步一个网站
|
||||||
async syncSite(siteId: number) {
|
async syncSite(siteId: number) {
|
||||||
try {
|
try {
|
||||||
// 通过数据库获取站点并转换为 WpSite,用于后续 WooCommerce 同步
|
// 通过数据库获取站点并转换为 Site,用于后续 WooCommerce 同步
|
||||||
const site = await this.siteService.get(siteId, true);
|
const site = await this.siteService.get(siteId, true);
|
||||||
const externalProductIds = this.wpProductModel.createQueryBuilder('wp_product')
|
const externalProductIds = this.wpProductModel.createQueryBuilder('wp_product')
|
||||||
.select([
|
.select([
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue