feat(customer): 实现客户数据同步功能并增强客户管理
重构客户服务层,添加客户数据同步功能 扩展客户实体字段以支持完整客户信息存储 优化客户列表查询性能并添加统计功能 移除废弃的WpSite相关代码和配置
This commit is contained in:
parent
0f5610e02e
commit
bc2ed4615e
|
|
@ -752,7 +752,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>(
|
||||
|
|
@ -794,3 +793,4 @@ export class WooCommerceAdapter implements ISiteAdapter {
|
|||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -116,17 +116,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: "23306",
|
||||
username: 'root',
|
||||
password: '12345678',
|
||||
database: 'inventory',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
@ -25,7 +27,7 @@ export default {
|
|||
origin: '*', // 允许所有来源跨域请求
|
||||
allowMethods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'], // 允许的 HTTP 方法
|
||||
allowHeaders: ['Content-Type', 'Authorization'], // 允许的自定义请求头
|
||||
credentials: true, // 允许携带凭据(cookies等)
|
||||
credentials: true, // 允许携带凭据(cookies等)
|
||||
},
|
||||
jwt: {
|
||||
secret: 'YOONE2024!@abc',
|
||||
|
|
@ -33,34 +35,38 @@ export default {
|
|||
},
|
||||
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',
|
||||
id: '200',
|
||||
wpApiUrl: "http://simple.local",
|
||||
consumerKey: 'ck_11b446d0dfd221853830b782049cf9a17553f886',
|
||||
consumerSecret: 'cs_2b06729269f659dcef675b8cdff542bf3c1da7e8',
|
||||
name: 'LocalSimple',
|
||||
email: '2469687281@qq.com',
|
||||
emailPswd: 'lulin91.',
|
||||
},
|
||||
// {
|
||||
// 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',
|
||||
// consumerKey: 'ck_dc9e151e9048c8ed3e27f35ac79d2bf7d6840652',
|
||||
// consumerSecret: 'cs_d05d625d7b0ac05c6d765671d8417f41d9477e38',
|
||||
// siteName: 'Local',
|
||||
// name: 'Local',
|
||||
// email: 'tom@yoonevape.com',
|
||||
// emailPswd: 'lulin91.',
|
||||
// },
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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';
|
||||
|
|
@ -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);
|
||||
|
|
@ -789,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 {
|
||||
|
|
@ -800,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
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -817,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
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -828,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);
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
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 WpSitesResponse extends SuccessArrayWrapper(SiteConfig) {}
|
||||
export class SitesResponse extends SuccessArrayWrapper(SiteConfig) {}
|
||||
//产品分页数据
|
||||
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')
|
||||
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;
|
||||
}
|
||||
|
|
@ -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 {
|
||||
/**
|
||||
|
|
@ -101,13 +102,13 @@ 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>>;
|
||||
getCustomer(id: string | number): Promise<UnifiedCustomerDTO>;
|
||||
|
|
@ -115,7 +116,7 @@ export interface ISiteAdapter {
|
|||
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列表
|
||||
|
|
|
|||
|
|
@ -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, 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 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 {
|
||||
current = 1,
|
||||
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) {
|
||||
const isExist = await this.customerTagModel.findOneBy({ email, tag });
|
||||
|
|
@ -172,4 +457,4 @@ export class CustomerService {
|
|||
async setRate(params: { id: number; rate: number }) {
|
||||
return await this.customerModel.update(params.id, { rate: params.rate });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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';
|
||||
|
|
@ -1444,8 +1443,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,
|
||||
|
|
|
|||
|
|
@ -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 中分离出区域代码和其他站点数据
|
||||
|
|
|
|||
|
|
@ -10,9 +10,10 @@ 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';
|
||||
|
||||
const MAX_PAGE_SIZE = 100;
|
||||
@Provide()
|
||||
export class WPService implements IPlatformService {
|
||||
getCustomer(site: any, id: number): Promise<any> {
|
||||
|
|
@ -79,11 +80,80 @@ 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;
|
||||
private 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 = {
|
||||
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(
|
||||
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;
|
||||
|
|
|
|||
|
|
@ -555,7 +555,7 @@ export class WpProductService {
|
|||
// 同步一个网站
|
||||
async syncSite(siteId: number) {
|
||||
try {
|
||||
// 通过数据库获取站点并转换为 WpSite,用于后续 WooCommerce 同步
|
||||
// 通过数据库获取站点并转换为 Site,用于后续 WooCommerce 同步
|
||||
const site = await this.siteService.get(siteId, true);
|
||||
const externalProductIds = this.wpProductModel.createQueryBuilder('wp_product')
|
||||
.select([
|
||||
|
|
|
|||
Loading…
Reference in New Issue