feat: 新增批量处理功能及用户邮箱字段
feat(adapter): 为Shopyy和WooCommerce适配器添加批量处理产品接口 feat(controller): 在产品控制器中新增站点SKU绑定接口 feat(controller): 在用户控制器中支持邮箱字段的增删改查 feat(controller): 新增客户标签管理接口 feat(controller): 在站点API控制器中添加批量导入导出功能 feat(service): 在产品服务中实现站点SKU绑定逻辑 feat(service): 在用户服务中添加邮箱字段校验和搜索 refactor(controller): 废弃部分WP产品控制器接口 refactor(webhook): 简化webhook控制器逻辑不再同步本地数据
This commit is contained in:
parent
87b4039a67
commit
3f3569995d
|
|
@ -248,6 +248,12 @@ export class ShopyyAdapter implements ISiteAdapter {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async batchProcessProducts(
|
||||||
|
data: { create?: any[]; update?: any[]; delete?: Array<string | number> }
|
||||||
|
): Promise<any> {
|
||||||
|
return await this.shopyyService.batchProcessProducts(this.site, data);
|
||||||
|
}
|
||||||
|
|
||||||
async getOrders(
|
async getOrders(
|
||||||
params: UnifiedSearchParamsDTO
|
params: UnifiedSearchParamsDTO
|
||||||
): Promise<UnifiedPaginationDTO<UnifiedOrderDTO>> {
|
): Promise<UnifiedPaginationDTO<UnifiedOrderDTO>> {
|
||||||
|
|
|
||||||
|
|
@ -169,6 +169,12 @@ export class WooCommerceAdapter implements ISiteAdapter {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async batchProcessProducts(
|
||||||
|
data: { create?: any[]; update?: any[]; delete?: Array<string | number> }
|
||||||
|
): Promise<any> {
|
||||||
|
return await this.wpService.batchProcessProducts(this.site, data);
|
||||||
|
}
|
||||||
|
|
||||||
async getOrders(
|
async getOrders(
|
||||||
params: UnifiedSearchParamsDTO
|
params: UnifiedSearchParamsDTO
|
||||||
): Promise<UnifiedPaginationDTO<UnifiedOrderDTO>> {
|
): Promise<UnifiedPaginationDTO<UnifiedOrderDTO>> {
|
||||||
|
|
|
||||||
|
|
@ -1,23 +1,63 @@
|
||||||
import { Controller, Get, Inject, Query } from '@midwayjs/core';
|
import { Controller, Get, Post, Inject, Query, Body } from '@midwayjs/core';
|
||||||
import { WPService } from '../service/wp.service';
|
|
||||||
import { successResponse, errorResponse } from '../utils/response.util';
|
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';
|
||||||
|
|
||||||
@Controller('/customer')
|
@Controller('/customer')
|
||||||
export class CustomerController {
|
export class CustomerController {
|
||||||
@Inject()
|
@Inject()
|
||||||
wpService: WPService;
|
customerService: CustomerService;
|
||||||
|
|
||||||
@Get('/list')
|
@ApiOkResponse({ type: Object })
|
||||||
async list(
|
@Get('/getcustomerlist')
|
||||||
@Query('siteId') siteId: number,
|
async getCustomerList(@Query() query: QueryCustomerListDTO) {
|
||||||
@Query('page') page: number = 1,
|
|
||||||
@Query('pageSize') pageSize: number = 20
|
|
||||||
) {
|
|
||||||
try {
|
try {
|
||||||
if (!siteId) {
|
const result = await this.customerService.getCustomerList(query as any);
|
||||||
return errorResponse('siteId is required');
|
return successResponse(result);
|
||||||
}
|
} catch (error) {
|
||||||
const result = await this.wpService.getCustomers(siteId, page, pageSize);
|
return errorResponse(error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiOkResponse({ type: Object })
|
||||||
|
@Post('/addtag')
|
||||||
|
async addTag(@Body() body: CustomerTagDTO) {
|
||||||
|
try {
|
||||||
|
const result = await this.customerService.addTag(body.email, body.tag);
|
||||||
|
return successResponse(result);
|
||||||
|
} catch (error) {
|
||||||
|
return errorResponse(error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiOkResponse({ type: Object })
|
||||||
|
@Post('/deltag')
|
||||||
|
async delTag(@Body() body: CustomerTagDTO) {
|
||||||
|
try {
|
||||||
|
const result = await this.customerService.delTag(body.email, body.tag);
|
||||||
|
return successResponse(result);
|
||||||
|
} catch (error) {
|
||||||
|
return errorResponse(error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiOkResponse({ type: Object })
|
||||||
|
@Get('/gettags')
|
||||||
|
async getTags() {
|
||||||
|
try {
|
||||||
|
const result = await this.customerService.getTags();
|
||||||
|
return successResponse(result);
|
||||||
|
} catch (error) {
|
||||||
|
return errorResponse(error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiOkResponse({ type: Object })
|
||||||
|
@Post('/setrate')
|
||||||
|
async setRate(@Body() body: { id: number; rate: number }) {
|
||||||
|
try {
|
||||||
|
const result = await this.customerService.setRate({ id: body.id, rate: body.rate });
|
||||||
return successResponse(result);
|
return successResponse(result);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return errorResponse(error.message);
|
return errorResponse(error.message);
|
||||||
|
|
|
||||||
|
|
@ -170,6 +170,30 @@ export class ProductController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 获取产品的站点SKU绑定
|
||||||
|
@ApiOkResponse()
|
||||||
|
@Get('/:id/site-skus')
|
||||||
|
async getProductSiteSkus(@Param('id') id: number) {
|
||||||
|
try {
|
||||||
|
const data = await this.productService.productSiteSkuModel.find({ where: { productId: id } });
|
||||||
|
return successResponse(data);
|
||||||
|
} catch (error) {
|
||||||
|
return errorResponse(error?.message || error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 覆盖式绑定产品的站点SKU列表
|
||||||
|
@ApiOkResponse()
|
||||||
|
@Post('/:id/site-skus')
|
||||||
|
async bindProductSiteSkus(@Param('id') id: number, @Body() body: { codes: string[] }) {
|
||||||
|
try {
|
||||||
|
const data = await this.productService.bindSiteSkus(id, body?.codes || []);
|
||||||
|
return successResponse(data);
|
||||||
|
} catch (error) {
|
||||||
|
return errorResponse(error?.message || error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@ApiOkResponse({ type: BooleanRes })
|
@ApiOkResponse({ type: BooleanRes })
|
||||||
@Del('/:id')
|
@Del('/:id')
|
||||||
async deleteProduct(@Param('id') id: number) {
|
async deleteProduct(@Param('id') id: number) {
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,53 @@ export class SiteApiController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Get('/:siteId/products/export')
|
||||||
|
async exportProducts(
|
||||||
|
@Param('siteId') siteId: number,
|
||||||
|
@Query() query: UnifiedSearchParamsDTO
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const adapter = await this.siteApiService.getAdapter(siteId);
|
||||||
|
const data = await adapter.getProducts(query);
|
||||||
|
const header = ['id','name','type','status','sku','regular_price','sale_price','price','stock_status','stock_quantity'];
|
||||||
|
const rows = data.items.map((p: any) => [p.id,p.name,p.type,p.status,p.sku,p.regular_price,p.sale_price,p.price,p.stock_status,p.stock_quantity]);
|
||||||
|
const csv = [header.join(','), ...rows.map(r => r.map(v => String(v ?? '')).join(','))].join('\n');
|
||||||
|
return successResponse({ csv });
|
||||||
|
} catch (error) {
|
||||||
|
return errorResponse(error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 平台特性:产品导出(特殊CSV,走平台服务)
|
||||||
|
@Get('/:siteId/products/export-special')
|
||||||
|
async exportProductsSpecial(
|
||||||
|
@Param('siteId') siteId: number,
|
||||||
|
@Query() query: UnifiedSearchParamsDTO
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const site = await this.siteApiService.siteService.get(siteId, true);
|
||||||
|
if (site.type === 'woocommerce') {
|
||||||
|
const page = query.page || 1;
|
||||||
|
const per_page = query.per_page || 100;
|
||||||
|
const res = await this.siteApiService.wpService.getProducts(site, page, per_page);
|
||||||
|
const header = ['id','name','type','status','sku','regular_price','sale_price','stock_status','stock_quantity'];
|
||||||
|
const rows = (res.items || []).map((p: any) => [p.id,p.name,p.type,p.status,p.sku,p.regular_price,p.sale_price,p.stock_status,p.stock_quantity]);
|
||||||
|
const csv = [header.join(','), ...rows.map(r => r.map(v => String(v ?? '')).join(','))].join('\n');
|
||||||
|
return successResponse({ csv });
|
||||||
|
}
|
||||||
|
if (site.type === 'shopyy') {
|
||||||
|
const res = await this.siteApiService.shopyyService.getProducts(site, query.page || 1, query.per_page || 100);
|
||||||
|
const header = ['id','name','type','status','sku','price','stock_status','stock_quantity'];
|
||||||
|
const rows = (res.items || []).map((p: any) => [p.id,p.name,p.type,p.status,p.sku,p.price,p.stock_status,p.stock_quantity]);
|
||||||
|
const csv = [header.join(','), ...rows.map(r => r.map(v => String(v ?? '')).join(','))].join('\n');
|
||||||
|
return successResponse({ csv });
|
||||||
|
}
|
||||||
|
throw new Error('Unsupported site type for special export');
|
||||||
|
} catch (error) {
|
||||||
|
return errorResponse(error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Get('/:siteId/products/:id')
|
@Get('/:siteId/products/:id')
|
||||||
@ApiOkResponse({ type: UnifiedProductDTO })
|
@ApiOkResponse({ type: UnifiedProductDTO })
|
||||||
async getProduct(
|
async getProduct(
|
||||||
|
|
@ -78,6 +125,86 @@ export class SiteApiController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Post('/:siteId/products/import')
|
||||||
|
@ApiOkResponse({ type: Object })
|
||||||
|
async importProducts(
|
||||||
|
@Param('siteId') siteId: number,
|
||||||
|
@Body() body: { items?: any[]; csv?: string }
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const adapter = await this.siteApiService.getAdapter(siteId);
|
||||||
|
let items = body.items || [];
|
||||||
|
if (!items.length && body.csv) {
|
||||||
|
const lines = body.csv.split(/\r?\n/).filter(Boolean);
|
||||||
|
const header = lines.shift()?.split(',') || [];
|
||||||
|
items = lines.map((line) => {
|
||||||
|
const cols = line.split(',');
|
||||||
|
const obj: any = {};
|
||||||
|
header.forEach((h, i) => (obj[h] = cols[i]));
|
||||||
|
return obj;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const created: any[] = [];
|
||||||
|
const failed: any[] = [];
|
||||||
|
for (const item of items) {
|
||||||
|
try {
|
||||||
|
const data = await adapter.createProduct(item);
|
||||||
|
created.push(data);
|
||||||
|
} catch (e) {
|
||||||
|
failed.push({ item, error: (e as any).message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return successResponse({ created, failed });
|
||||||
|
} catch (error) {
|
||||||
|
return errorResponse(error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 平台特性:产品导入(特殊CSV,走平台服务)
|
||||||
|
@Post('/:siteId/products/import-special')
|
||||||
|
@ApiOkResponse({ type: Object })
|
||||||
|
async importProductsSpecial(
|
||||||
|
@Param('siteId') siteId: number,
|
||||||
|
@Body() body: { csv?: string; items?: any[] }
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const site = await this.siteApiService.siteService.get(siteId, true);
|
||||||
|
const csvText = body.csv || '';
|
||||||
|
const items = body.items || [];
|
||||||
|
const created: any[] = [];
|
||||||
|
const failed: any[] = [];
|
||||||
|
if (site.type === 'woocommerce') {
|
||||||
|
// 解析 CSV 为对象数组(若传入 items 则优先 items)
|
||||||
|
let payloads = items;
|
||||||
|
if (!payloads.length && csvText) {
|
||||||
|
const lines = csvText.split(/\r?\n/).filter(Boolean);
|
||||||
|
const header = lines.shift()?.split(',') || [];
|
||||||
|
payloads = lines.map((line) => {
|
||||||
|
const cols = line.split(',');
|
||||||
|
const obj: any = {};
|
||||||
|
header.forEach((h, i) => (obj[h] = cols[i]));
|
||||||
|
return obj;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
for (const item of payloads) {
|
||||||
|
try {
|
||||||
|
const res = await this.siteApiService.wpService.createProduct(site, item);
|
||||||
|
created.push(res);
|
||||||
|
} catch (e) {
|
||||||
|
failed.push({ item, error: (e as any).message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return successResponse({ created, failed });
|
||||||
|
}
|
||||||
|
if (site.type === 'shopyy') {
|
||||||
|
throw new Error('ShopYY 暂不支持特殊CSV导入');
|
||||||
|
}
|
||||||
|
throw new Error('Unsupported site type for special import');
|
||||||
|
} catch (error) {
|
||||||
|
return errorResponse(error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Put('/:siteId/products/:id')
|
@Put('/:siteId/products/:id')
|
||||||
@ApiOkResponse({ type: UnifiedProductDTO })
|
@ApiOkResponse({ type: UnifiedProductDTO })
|
||||||
async updateProduct(
|
async updateProduct(
|
||||||
|
|
@ -135,6 +262,64 @@ export class SiteApiController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Post('/:siteId/products/batch')
|
||||||
|
@ApiOkResponse({ type: Object })
|
||||||
|
async batchProducts(
|
||||||
|
@Param('siteId') siteId: number,
|
||||||
|
@Body() body: { create?: any[]; update?: any[]; delete?: Array<string | number> }
|
||||||
|
) {
|
||||||
|
this.logger.info(`[Site API] 批量处理产品开始, siteId: ${siteId}`);
|
||||||
|
try {
|
||||||
|
const adapter = await this.siteApiService.getAdapter(siteId);
|
||||||
|
if (adapter.batchProcessProducts) {
|
||||||
|
const res = await adapter.batchProcessProducts(body);
|
||||||
|
this.logger.info(`[Site API] 批量处理产品成功, siteId: ${siteId}`);
|
||||||
|
return successResponse(res);
|
||||||
|
}
|
||||||
|
const created: any[] = [];
|
||||||
|
const updated: any[] = [];
|
||||||
|
const deleted: Array<string | number> = [];
|
||||||
|
const failed: any[] = [];
|
||||||
|
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 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (body.update?.length) {
|
||||||
|
for (const item of body.update) {
|
||||||
|
try {
|
||||||
|
const id = item.id;
|
||||||
|
const data = await adapter.updateProduct(id, item);
|
||||||
|
updated.push(data);
|
||||||
|
} catch (e) {
|
||||||
|
failed.push({ action: 'update', item, error: (e as any).message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (body.delete?.length) {
|
||||||
|
for (const id of body.delete) {
|
||||||
|
try {
|
||||||
|
const ok = await adapter.deleteProduct(id);
|
||||||
|
if (ok) deleted.push(id);
|
||||||
|
else failed.push({ action: 'delete', id, error: 'delete failed' });
|
||||||
|
} catch (e) {
|
||||||
|
failed.push({ action: 'delete', id, error: (e as any).message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.logger.info(`[Site API] 批量处理产品完成, siteId: ${siteId}`);
|
||||||
|
return successResponse({ created, updated, deleted, failed });
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(`[Site API] 批量处理产品失败, siteId: ${siteId}, 错误信息: ${error.message}`);
|
||||||
|
return errorResponse(error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Get('/:siteId/orders')
|
@Get('/:siteId/orders')
|
||||||
@ApiOkResponse({ type: UnifiedOrderPaginationDTO })
|
@ApiOkResponse({ type: UnifiedOrderPaginationDTO })
|
||||||
async getOrders(
|
async getOrders(
|
||||||
|
|
@ -153,6 +338,23 @@ export class SiteApiController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Get('/:siteId/orders/export')
|
||||||
|
async exportOrders(
|
||||||
|
@Param('siteId') siteId: number,
|
||||||
|
@Query() query: UnifiedSearchParamsDTO
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const adapter = await this.siteApiService.getAdapter(siteId);
|
||||||
|
const data = await adapter.getOrders(query);
|
||||||
|
const header = ['id','number','status','currency','total','customer_id','customer_name','email','date_created'];
|
||||||
|
const rows = data.items.map((o: any) => [o.id,o.number,o.status,o.currency,o.total,o.customer_id,o.customer_name,o.email,o.date_created]);
|
||||||
|
const csv = [header.join(','), ...rows.map(r => r.map(v => String(v ?? '')).join(','))].join('\n');
|
||||||
|
return successResponse({ csv });
|
||||||
|
} catch (error) {
|
||||||
|
return errorResponse(error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Get('/:siteId/orders/:id')
|
@Get('/:siteId/orders/:id')
|
||||||
@ApiOkResponse({ type: UnifiedOrderDTO })
|
@ApiOkResponse({ type: UnifiedOrderDTO })
|
||||||
async getOrder(
|
async getOrder(
|
||||||
|
|
@ -189,6 +391,41 @@ export class SiteApiController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Post('/:siteId/orders/import')
|
||||||
|
@ApiOkResponse({ type: Object })
|
||||||
|
async importOrders(
|
||||||
|
@Param('siteId') siteId: number,
|
||||||
|
@Body() body: { items?: any[]; csv?: string }
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const adapter = await this.siteApiService.getAdapter(siteId);
|
||||||
|
let items = body.items || [];
|
||||||
|
if (!items.length && body.csv) {
|
||||||
|
const lines = body.csv.split(/\r?\n/).filter(Boolean);
|
||||||
|
const header = lines.shift()?.split(',') || [];
|
||||||
|
items = lines.map((line) => {
|
||||||
|
const cols = line.split(',');
|
||||||
|
const obj: any = {};
|
||||||
|
header.forEach((h, i) => (obj[h] = cols[i]));
|
||||||
|
return obj;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const created: any[] = [];
|
||||||
|
const failed: any[] = [];
|
||||||
|
for (const item of items) {
|
||||||
|
try {
|
||||||
|
const data = await adapter.createOrder(item);
|
||||||
|
created.push(data);
|
||||||
|
} catch (e) {
|
||||||
|
failed.push({ item, error: (e as any).message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return successResponse({ created, failed });
|
||||||
|
} catch (error) {
|
||||||
|
return errorResponse(error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Put('/:siteId/orders/:id')
|
@Put('/:siteId/orders/:id')
|
||||||
@ApiOkResponse({ type: Boolean })
|
@ApiOkResponse({ type: Boolean })
|
||||||
async updateOrder(
|
async updateOrder(
|
||||||
|
|
@ -226,6 +463,60 @@ export class SiteApiController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Post('/:siteId/orders/batch')
|
||||||
|
@ApiOkResponse({ type: Object })
|
||||||
|
async batchOrders(
|
||||||
|
@Param('siteId') siteId: number,
|
||||||
|
@Body() body: { create?: any[]; update?: any[]; delete?: Array<string | number> }
|
||||||
|
) {
|
||||||
|
this.logger.info(`[Site API] 批量处理订单开始, siteId: ${siteId}`);
|
||||||
|
try {
|
||||||
|
const adapter = await this.siteApiService.getAdapter(siteId);
|
||||||
|
const created: any[] = [];
|
||||||
|
const updated: any[] = [];
|
||||||
|
const deleted: Array<string | number> = [];
|
||||||
|
const failed: any[] = [];
|
||||||
|
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 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (body.update?.length) {
|
||||||
|
for (const item of body.update) {
|
||||||
|
try {
|
||||||
|
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' });
|
||||||
|
} catch (e) {
|
||||||
|
failed.push({ action: 'update', item, error: (e as any).message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (body.delete?.length) {
|
||||||
|
for (const id of body.delete) {
|
||||||
|
try {
|
||||||
|
const ok = await adapter.deleteOrder(id);
|
||||||
|
if (ok) deleted.push(id);
|
||||||
|
else failed.push({ action: 'delete', id, error: 'delete failed' });
|
||||||
|
} catch (e) {
|
||||||
|
failed.push({ action: 'delete', id, error: (e as any).message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.logger.info(`[Site API] 批量处理订单完成, siteId: ${siteId}`);
|
||||||
|
return successResponse({ created, updated, deleted, failed });
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(`[Site API] 批量处理订单失败, siteId: ${siteId}, 错误信息: ${error.message}`);
|
||||||
|
return errorResponse(error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Get('/:siteId/orders/:id/notes')
|
@Get('/:siteId/orders/:id/notes')
|
||||||
@ApiOkResponse({ type: Object })
|
@ApiOkResponse({ type: Object })
|
||||||
async getOrderNotes(
|
async getOrderNotes(
|
||||||
|
|
@ -281,6 +572,23 @@ export class SiteApiController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Get('/:siteId/subscriptions/export')
|
||||||
|
async exportSubscriptions(
|
||||||
|
@Param('siteId') siteId: number,
|
||||||
|
@Query() query: UnifiedSearchParamsDTO
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const adapter = await this.siteApiService.getAdapter(siteId);
|
||||||
|
const data = await adapter.getSubscriptions(query);
|
||||||
|
const header = ['id','status','customer_id','billing_period','billing_interval','start_date','next_payment_date'];
|
||||||
|
const rows = data.items.map((s: any) => [s.id,s.status,s.customer_id,s.billing_period,s.billing_interval,s.start_date,s.next_payment_date]);
|
||||||
|
const csv = [header.join(','), ...rows.map(r => r.map(v => String(v ?? '')).join(','))].join('\n');
|
||||||
|
return successResponse({ csv });
|
||||||
|
} catch (error) {
|
||||||
|
return errorResponse(error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Get('/:siteId/media')
|
@Get('/:siteId/media')
|
||||||
@ApiOkResponse({ type: UnifiedMediaPaginationDTO })
|
@ApiOkResponse({ type: UnifiedMediaPaginationDTO })
|
||||||
async getMedia(
|
async getMedia(
|
||||||
|
|
@ -299,6 +607,23 @@ export class SiteApiController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Get('/:siteId/media/export')
|
||||||
|
async exportMedia(
|
||||||
|
@Param('siteId') siteId: number,
|
||||||
|
@Query() query: UnifiedSearchParamsDTO
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const adapter = await this.siteApiService.getAdapter(siteId);
|
||||||
|
const data = await adapter.getMedia(query);
|
||||||
|
const header = ['id','title','media_type','mime_type','source_url','date_created'];
|
||||||
|
const rows = data.items.map((m: any) => [m.id,m.title,m.media_type,m.mime_type,m.source_url,m.date_created]);
|
||||||
|
const csv = [header.join(','), ...rows.map(r => r.map(v => String(v ?? '')).join(','))].join('\n');
|
||||||
|
return successResponse({ csv });
|
||||||
|
} catch (error) {
|
||||||
|
return errorResponse(error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Del('/:siteId/media/:id')
|
@Del('/:siteId/media/:id')
|
||||||
@ApiOkResponse({ type: Boolean })
|
@ApiOkResponse({ type: Boolean })
|
||||||
async deleteMedia(
|
async deleteMedia(
|
||||||
|
|
@ -344,6 +669,50 @@ export class SiteApiController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Post('/:siteId/media/batch')
|
||||||
|
@ApiOkResponse({ type: Object })
|
||||||
|
async batchMedia(
|
||||||
|
@Param('siteId') siteId: number,
|
||||||
|
@Body() body: { update?: any[]; delete?: Array<string | number> }
|
||||||
|
) {
|
||||||
|
this.logger.info(`[Site API] 批量处理媒体开始, siteId: ${siteId}`);
|
||||||
|
try {
|
||||||
|
const adapter = await this.siteApiService.getAdapter(siteId);
|
||||||
|
const updated: any[] = [];
|
||||||
|
const deleted: Array<string | number> = [];
|
||||||
|
const failed: any[] = [];
|
||||||
|
const api: any = adapter as any;
|
||||||
|
if (body.update?.length) {
|
||||||
|
for (const item of body.update) {
|
||||||
|
try {
|
||||||
|
if (!api.updateMedia) throw new Error('Media update not supported');
|
||||||
|
const res = await api.updateMedia(item.id, item);
|
||||||
|
updated.push(res);
|
||||||
|
} catch (e) {
|
||||||
|
failed.push({ action: 'update', item, error: (e as any).message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (body.delete?.length) {
|
||||||
|
for (const id of body.delete) {
|
||||||
|
try {
|
||||||
|
if (!api.deleteMedia) throw new Error('Media delete not supported');
|
||||||
|
const ok = await api.deleteMedia(id);
|
||||||
|
if (ok) deleted.push(id);
|
||||||
|
else failed.push({ action: 'delete', id, error: 'delete failed' });
|
||||||
|
} catch (e) {
|
||||||
|
failed.push({ action: 'delete', id, error: (e as any).message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.logger.info(`[Site API] 批量处理媒体完成, siteId: ${siteId}`);
|
||||||
|
return successResponse({ updated, deleted, failed });
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(`[Site API] 批量处理媒体失败, siteId: ${siteId}, 错误信息: ${error.message}`);
|
||||||
|
return errorResponse(error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Get('/:siteId/customers')
|
@Get('/:siteId/customers')
|
||||||
@ApiOkResponse({ type: UnifiedCustomerPaginationDTO })
|
@ApiOkResponse({ type: UnifiedCustomerPaginationDTO })
|
||||||
async getCustomers(
|
async getCustomers(
|
||||||
|
|
@ -362,6 +731,23 @@ export class SiteApiController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Get('/:siteId/customers/export')
|
||||||
|
async exportCustomers(
|
||||||
|
@Param('siteId') siteId: number,
|
||||||
|
@Query() query: UnifiedSearchParamsDTO
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const adapter = await this.siteApiService.getAdapter(siteId);
|
||||||
|
const data = await adapter.getCustomers(query);
|
||||||
|
const header = ['id','email','first_name','last_name','fullname','username','phone'];
|
||||||
|
const rows = data.items.map((c: any) => [c.id,c.email,c.first_name,c.last_name,c.fullname,c.username,c.phone]);
|
||||||
|
const csv = [header.join(','), ...rows.map(r => r.map(v => String(v ?? '')).join(','))].join('\n');
|
||||||
|
return successResponse({ csv });
|
||||||
|
} catch (error) {
|
||||||
|
return errorResponse(error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Get('/:siteId/customers/:id')
|
@Get('/:siteId/customers/:id')
|
||||||
@ApiOkResponse({ type: UnifiedCustomerDTO })
|
@ApiOkResponse({ type: UnifiedCustomerDTO })
|
||||||
async getCustomer(
|
async getCustomer(
|
||||||
|
|
@ -398,6 +784,41 @@ export class SiteApiController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Post('/:siteId/customers/import')
|
||||||
|
@ApiOkResponse({ type: Object })
|
||||||
|
async importCustomers(
|
||||||
|
@Param('siteId') siteId: number,
|
||||||
|
@Body() body: { items?: any[]; csv?: string }
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const adapter = await this.siteApiService.getAdapter(siteId);
|
||||||
|
let items = body.items || [];
|
||||||
|
if (!items.length && body.csv) {
|
||||||
|
const lines = body.csv.split(/\r?\n/).filter(Boolean);
|
||||||
|
const header = lines.shift()?.split(',') || [];
|
||||||
|
items = lines.map((line) => {
|
||||||
|
const cols = line.split(',');
|
||||||
|
const obj: any = {};
|
||||||
|
header.forEach((h, i) => (obj[h] = cols[i]));
|
||||||
|
return obj;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const created: any[] = [];
|
||||||
|
const failed: any[] = [];
|
||||||
|
for (const item of items) {
|
||||||
|
try {
|
||||||
|
const data = await adapter.createCustomer(item);
|
||||||
|
created.push(data);
|
||||||
|
} catch (e) {
|
||||||
|
failed.push({ item, error: (e as any).message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return successResponse({ created, failed });
|
||||||
|
} catch (error) {
|
||||||
|
return errorResponse(error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Put('/:siteId/customers/:id')
|
@Put('/:siteId/customers/:id')
|
||||||
@ApiOkResponse({ type: UnifiedCustomerDTO })
|
@ApiOkResponse({ type: UnifiedCustomerDTO })
|
||||||
async updateCustomer(
|
async updateCustomer(
|
||||||
|
|
@ -434,4 +855,57 @@ export class SiteApiController {
|
||||||
return errorResponse(error.message);
|
return errorResponse(error.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Post('/:siteId/customers/batch')
|
||||||
|
@ApiOkResponse({ type: Object })
|
||||||
|
async batchCustomers(
|
||||||
|
@Param('siteId') siteId: number,
|
||||||
|
@Body() body: { create?: any[]; update?: any[]; delete?: Array<string | number> }
|
||||||
|
) {
|
||||||
|
this.logger.info(`[Site API] 批量处理客户开始, siteId: ${siteId}`);
|
||||||
|
try {
|
||||||
|
const adapter = await this.siteApiService.getAdapter(siteId);
|
||||||
|
const created: any[] = [];
|
||||||
|
const updated: any[] = [];
|
||||||
|
const deleted: Array<string | number> = [];
|
||||||
|
const failed: any[] = [];
|
||||||
|
if (body.create?.length) {
|
||||||
|
for (const item of body.create) {
|
||||||
|
try {
|
||||||
|
const data = await adapter.createCustomer(item);
|
||||||
|
created.push(data);
|
||||||
|
} catch (e) {
|
||||||
|
failed.push({ action: 'create', item, error: (e as any).message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (body.update?.length) {
|
||||||
|
for (const item of body.update) {
|
||||||
|
try {
|
||||||
|
const id = item.id;
|
||||||
|
const data = await adapter.updateCustomer(id, item);
|
||||||
|
updated.push(data);
|
||||||
|
} catch (e) {
|
||||||
|
failed.push({ action: 'update', item, error: (e as any).message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (body.delete?.length) {
|
||||||
|
for (const id of body.delete) {
|
||||||
|
try {
|
||||||
|
const ok = await adapter.deleteCustomer(id);
|
||||||
|
if (ok) deleted.push(id);
|
||||||
|
else failed.push({ action: 'delete', id, error: 'delete failed' });
|
||||||
|
} catch (e) {
|
||||||
|
failed.push({ action: 'delete', id, error: (e as any).message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.logger.info(`[Site API] 批量处理客户完成, siteId: ${siteId}`);
|
||||||
|
return successResponse({ created, updated, deleted, failed });
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(`[Site API] 批量处理客户失败, siteId: ${siteId}, 错误信息: ${error.message}`);
|
||||||
|
return errorResponse(error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -40,11 +40,11 @@ export class UserController {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post('/add')
|
@Post('/add')
|
||||||
async addUser(@Body() body: { username: string; password: string; remark?: string }) {
|
async addUser(@Body() body: { username: string; password: string; email?: string; remark?: string }) {
|
||||||
const { username, password, remark } = body;
|
const { username, password, email, remark } = body;
|
||||||
try {
|
try {
|
||||||
// 新增用户(支持备注)
|
// 新增用户 支持邮箱与备注
|
||||||
await this.userService.addUser(username, password, remark);
|
await this.userService.addUser(username, password, remark, email);
|
||||||
return successResponse(true);
|
return successResponse(true);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error);
|
console.log(error);
|
||||||
|
|
@ -60,6 +60,7 @@ export class UserController {
|
||||||
pageSize: number;
|
pageSize: number;
|
||||||
remark?: string;
|
remark?: string;
|
||||||
username?: string;
|
username?: string;
|
||||||
|
email?: string;
|
||||||
isActive?: string;
|
isActive?: string;
|
||||||
isSuper?: string;
|
isSuper?: string;
|
||||||
isAdmin?: string;
|
isAdmin?: string;
|
||||||
|
|
@ -67,7 +68,7 @@ export class UserController {
|
||||||
sortOrder?: string;
|
sortOrder?: string;
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
const { current = 1, pageSize = 10, remark, username, isActive, isSuper, isAdmin, sortField, sortOrder } = query;
|
const { current = 1, pageSize = 10, remark, username, email, isActive, isSuper, isAdmin, sortField, sortOrder } = query;
|
||||||
// 将字符串布尔转换为真实布尔
|
// 将字符串布尔转换为真实布尔
|
||||||
const toBool = (v?: string) => (v === undefined ? undefined : v === 'true');
|
const toBool = (v?: string) => (v === undefined ? undefined : v === 'true');
|
||||||
// 处理排序方向
|
// 处理排序方向
|
||||||
|
|
@ -80,6 +81,7 @@ export class UserController {
|
||||||
{
|
{
|
||||||
remark,
|
remark,
|
||||||
username,
|
username,
|
||||||
|
email,
|
||||||
isActive: toBool(isActive),
|
isActive: toBool(isActive),
|
||||||
isSuper: toBool(isSuper),
|
isSuper: toBool(isSuper),
|
||||||
isAdmin: toBool(isAdmin),
|
isAdmin: toBool(isAdmin),
|
||||||
|
|
@ -112,7 +114,7 @@ export class UserController {
|
||||||
// 更新用户(支持用户名/密码/权限/角色更新)
|
// 更新用户(支持用户名/密码/权限/角色更新)
|
||||||
@Post('/update/:id')
|
@Post('/update/:id')
|
||||||
async updateUser(
|
async updateUser(
|
||||||
@Body() body: { username?: string; password?: string; isSuper?: boolean; isAdmin?: boolean; permissions?: string[]; remark?: string },
|
@Body() body: { username?: string; password?: string; email?: string; isSuper?: boolean; isAdmin?: boolean; permissions?: string[]; remark?: string },
|
||||||
@Query('id') id?: number
|
@Query('id') id?: number
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
|
|
@ -9,8 +9,7 @@ import {
|
||||||
} from '@midwayjs/decorator';
|
} from '@midwayjs/decorator';
|
||||||
import { Context } from '@midwayjs/koa';
|
import { Context } from '@midwayjs/koa';
|
||||||
import * as crypto from 'crypto';
|
import * as crypto from 'crypto';
|
||||||
import { WpProductService } from '../service/wp_product.service';
|
|
||||||
import { WPService } from '../service/wp.service';
|
|
||||||
import { SiteService } from '../service/site.service';
|
import { SiteService } from '../service/site.service';
|
||||||
import { OrderService } from '../service/order.service';
|
import { OrderService } from '../service/order.service';
|
||||||
|
|
||||||
|
|
@ -18,11 +17,7 @@ import { OrderService } from '../service/order.service';
|
||||||
export class WebhookController {
|
export class WebhookController {
|
||||||
private secret = 'YOONE24kd$kjcdjflddd';
|
private secret = 'YOONE24kd$kjcdjflddd';
|
||||||
|
|
||||||
@Inject()
|
// 平台服务保留按需注入
|
||||||
private readonly wpProductService: WpProductService;
|
|
||||||
|
|
||||||
@Inject()
|
|
||||||
private readonly wpApiService: WPService;
|
|
||||||
|
|
||||||
@Inject()
|
@Inject()
|
||||||
private readonly orderService: OrderService;
|
private readonly orderService: OrderService;
|
||||||
|
|
@ -79,32 +74,10 @@ export class WebhookController {
|
||||||
switch (topic) {
|
switch (topic) {
|
||||||
case 'product.created':
|
case 'product.created':
|
||||||
case 'product.updated':
|
case 'product.updated':
|
||||||
// 变体更新
|
// 不再写入本地,平台事件仅确认接收
|
||||||
if (body.type === 'variation') {
|
|
||||||
const variation = await this.wpApiService.getVariation(
|
|
||||||
site,
|
|
||||||
body.parent_id,
|
|
||||||
body.id
|
|
||||||
);
|
|
||||||
this.wpProductService.syncVariation(
|
|
||||||
siteId,
|
|
||||||
body.parent_id,
|
|
||||||
variation
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
const variations =
|
|
||||||
body.type === 'variable'
|
|
||||||
? await this.wpApiService.getVariations(site, body.id)
|
|
||||||
: [];
|
|
||||||
await this.wpProductService.syncProductAndVariations(
|
|
||||||
site.id,
|
|
||||||
body,
|
|
||||||
variations
|
|
||||||
);
|
|
||||||
break;
|
break;
|
||||||
case 'product.deleted':
|
case 'product.deleted':
|
||||||
await this.wpProductService.delWpProduct(site.id, body.id);
|
// 不再写入本地,平台事件仅确认接收
|
||||||
break;
|
break;
|
||||||
case 'order.created':
|
case 'order.created':
|
||||||
case 'order.updated':
|
case 'order.updated':
|
||||||
|
|
|
||||||
|
|
@ -22,8 +22,7 @@ import {
|
||||||
BatchUpdateTagsDTO,
|
BatchUpdateTagsDTO,
|
||||||
BatchUpdateProductsDTO,
|
BatchUpdateProductsDTO,
|
||||||
} from '../dto/wp_product.dto';
|
} from '../dto/wp_product.dto';
|
||||||
import { WPService } from '../service/wp.service';
|
|
||||||
import { SiteService } from '../service/site.service';
|
|
||||||
import {
|
import {
|
||||||
ProductsRes,
|
ProductsRes,
|
||||||
} from '../dto/reponse.dto';
|
} from '../dto/reponse.dto';
|
||||||
|
|
@ -34,23 +33,14 @@ export class WpProductController {
|
||||||
@Inject()
|
@Inject()
|
||||||
private readonly wpProductService: WpProductService;
|
private readonly wpProductService: WpProductService;
|
||||||
|
|
||||||
@Inject()
|
// 平台服务保留按需注入
|
||||||
private readonly wpApiService: WPService;
|
|
||||||
|
|
||||||
@Inject()
|
|
||||||
private readonly siteService: SiteService;
|
|
||||||
|
|
||||||
@ApiOkResponse({
|
@ApiOkResponse({
|
||||||
type: BooleanRes,
|
type: BooleanRes,
|
||||||
})
|
})
|
||||||
@Del('/:id')
|
@Del('/:id')
|
||||||
async delete(@Param('id') id: number) {
|
async delete(@Param('id') id: number) {
|
||||||
try {
|
return errorResponse('接口已废弃,请改用 /site-api/:siteId/products 删除');
|
||||||
await this.wpProductService.deleteById(id);
|
|
||||||
return successResponse(true);
|
|
||||||
} catch (error) {
|
|
||||||
return errorResponse(error.message || '删除失败');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@ApiOkResponse({
|
@ApiOkResponse({
|
||||||
|
|
@ -70,6 +60,18 @@ export class WpProductController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ApiOkResponse({
|
||||||
|
type: BooleanRes,
|
||||||
|
})
|
||||||
|
@Post('/setconstitution')
|
||||||
|
async setConstitution(@Body() body: any) {
|
||||||
|
try {
|
||||||
|
return successResponse(true);
|
||||||
|
} catch (error) {
|
||||||
|
return errorResponse(error.message || '设置失败');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@ApiOkResponse({
|
@ApiOkResponse({
|
||||||
type: BooleanRes,
|
type: BooleanRes,
|
||||||
})
|
})
|
||||||
|
|
@ -132,12 +134,7 @@ export class WpProductController {
|
||||||
})
|
})
|
||||||
@Get('/list')
|
@Get('/list')
|
||||||
async getWpProducts(@Query() query: QueryWpProductDTO) {
|
async getWpProducts(@Query() query: QueryWpProductDTO) {
|
||||||
try {
|
return errorResponse('接口已废弃,请改用 /site-api/:siteId/products 列表');
|
||||||
const data = await this.wpProductService.getProductList(query);
|
|
||||||
return successResponse(data);
|
|
||||||
} catch (error) {
|
|
||||||
return errorResponse(error.message);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@ApiOkResponse({
|
@ApiOkResponse({
|
||||||
|
|
@ -169,29 +166,7 @@ export class WpProductController {
|
||||||
@Param('siteId') siteId: number,
|
@Param('siteId') siteId: number,
|
||||||
@Body() body: any
|
@Body() body: any
|
||||||
) {
|
) {
|
||||||
try {
|
return errorResponse('接口已废弃,请改用 /site-api/:siteId/products 创建');
|
||||||
// 过滤掉前端可能传入的多余字段
|
|
||||||
const { fromProductId, ...productData } = body;
|
|
||||||
|
|
||||||
if (productData.type === 'single') {
|
|
||||||
productData.type = 'simple';
|
|
||||||
}
|
|
||||||
const site = await this.siteService.get(siteId, true);
|
|
||||||
const result = await this.wpApiService.createProduct(
|
|
||||||
site,
|
|
||||||
productData
|
|
||||||
);
|
|
||||||
if (result) {
|
|
||||||
// 同步回本地数据库
|
|
||||||
await this.wpProductService.syncProductAndVariations(siteId, result, []);
|
|
||||||
return successResponse(result, '产品创建成功');
|
|
||||||
}
|
|
||||||
return errorResponse('产品创建失败');
|
|
||||||
} catch (error) {
|
|
||||||
console.error('创建产品失败:', error);
|
|
||||||
// 返回更详细的错误信息,特别是来自 WooCommerce 的错误
|
|
||||||
return errorResponse(error.response?.data?.message || error.message || '产品创建失败');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -208,45 +183,7 @@ export class WpProductController {
|
||||||
@Param('productId') productId: string,
|
@Param('productId') productId: string,
|
||||||
@Body() body: UpdateWpProductDTO
|
@Body() body: UpdateWpProductDTO
|
||||||
) {
|
) {
|
||||||
try {
|
return errorResponse('接口已废弃,请改用 /site-api/:siteId/products/:id 更新');
|
||||||
// ? 这个是啥意思
|
|
||||||
const isDuplicate = await this.wpProductService.isSkuDuplicate(
|
|
||||||
body.sku,
|
|
||||||
siteId,
|
|
||||||
productId
|
|
||||||
);
|
|
||||||
if (isDuplicate) {
|
|
||||||
return errorResponse('SKU已存在');
|
|
||||||
}
|
|
||||||
|
|
||||||
const site = await this.siteService.get(siteId, true);
|
|
||||||
|
|
||||||
// Resolve tags
|
|
||||||
if (body.tags && body.tags.length > 0) {
|
|
||||||
const resolvedTags = await this.wpApiService.ensureTags(site, body.tags);
|
|
||||||
(body as any).tags = resolvedTags;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Resolve categories
|
|
||||||
if (body.categories && body.categories.length > 0) {
|
|
||||||
const resolvedCategories = await this.wpApiService.ensureCategories(site, body.categories);
|
|
||||||
(body as any).categories = resolvedCategories;
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await this.wpApiService.updateProduct(
|
|
||||||
site,
|
|
||||||
productId,
|
|
||||||
body
|
|
||||||
);
|
|
||||||
if (result) {
|
|
||||||
this.wpProductService.updateWpProduct(siteId, productId, body);
|
|
||||||
return successResponse(result, '产品更新成功');
|
|
||||||
}
|
|
||||||
return errorResponse('产品更新失败');
|
|
||||||
} catch (error) {
|
|
||||||
console.error('更新产品失败:', error);
|
|
||||||
return errorResponse(error.message || '产品更新失败');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@ApiOkResponse({
|
@ApiOkResponse({
|
||||||
|
|
@ -275,37 +212,7 @@ export class WpProductController {
|
||||||
@Param('variationId') variationId: string,
|
@Param('variationId') variationId: string,
|
||||||
@Body() body: UpdateVariationDTO
|
@Body() body: UpdateVariationDTO
|
||||||
) {
|
) {
|
||||||
try {
|
return errorResponse('接口已废弃,请改用 /site-api/:siteId/products/:productId/variations/:variationId 更新');
|
||||||
const isDuplicate = await this.wpProductService.isSkuDuplicate(
|
|
||||||
body.sku,
|
|
||||||
siteId,
|
|
||||||
productId,
|
|
||||||
variationId
|
|
||||||
);
|
|
||||||
if (isDuplicate) {
|
|
||||||
return errorResponse('SKU已存在');
|
|
||||||
}
|
|
||||||
const site = await this.siteService.get(siteId, true);
|
|
||||||
const result = await this.wpApiService.updateVariation(
|
|
||||||
site,
|
|
||||||
productId,
|
|
||||||
variationId,
|
|
||||||
body
|
|
||||||
);
|
|
||||||
if (result) {
|
|
||||||
this.wpProductService.updateWpProductVaritation(
|
|
||||||
siteId,
|
|
||||||
productId,
|
|
||||||
variationId,
|
|
||||||
body
|
|
||||||
);
|
|
||||||
return successResponse(result, '产品变体更新成功');
|
|
||||||
}
|
|
||||||
return errorResponse('变体更新失败');
|
|
||||||
} catch (error) {
|
|
||||||
console.error('更新变体失败:', error);
|
|
||||||
return errorResponse(error.message || '产品变体更新失败');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@ApiOkResponse({
|
@ApiOkResponse({
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,10 @@ export class User {
|
||||||
@Column({ type: 'simple-array', nullable: true })
|
@Column({ type: 'simple-array', nullable: true })
|
||||||
permissions: string[]; // 自定义权限 (如:['user:add', 'user:edit'])
|
permissions: string[]; // 自定义权限 (如:['user:add', 'user:edit'])
|
||||||
|
|
||||||
|
// 新增邮箱字段,可选且唯一
|
||||||
|
@Column({ unique: true, nullable: true })
|
||||||
|
email?: string;
|
||||||
|
|
||||||
@Column({ default: false })
|
@Column({ default: false })
|
||||||
isSuper: boolean; // 超级管理员
|
isSuper: boolean; // 超级管理员
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -69,13 +69,19 @@ 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>;
|
||||||
|
|
||||||
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>;
|
||||||
|
|
||||||
getCustomers(params: UnifiedSearchParamsDTO): Promise<UnifiedPaginationDTO<UnifiedCustomerDTO>>;
|
getCustomers(params: UnifiedSearchParamsDTO): Promise<UnifiedPaginationDTO<UnifiedCustomerDTO>>;
|
||||||
getCustomer(id: string | number): Promise<UnifiedCustomerDTO>;
|
getCustomer(id: string | number): Promise<UnifiedCustomerDTO>;
|
||||||
createCustomer(data: Partial<UnifiedCustomerDTO>): Promise<UnifiedCustomerDTO>;
|
createCustomer(data: Partial<UnifiedCustomerDTO>): Promise<UnifiedCustomerDTO>;
|
||||||
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>;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1 @@
|
||||||
import { FORMAT, ILogger, Logger } from '@midwayjs/core';
|
export {}
|
||||||
import { IJob, Job } from '@midwayjs/cron';
|
|
||||||
|
|
||||||
@Job({
|
|
||||||
cronTime: FORMAT.CRONTAB.EVERY_DAY,
|
|
||||||
runOnInit: true,
|
|
||||||
})
|
|
||||||
export class SyncProductJob implements IJob {
|
|
||||||
@Logger()
|
|
||||||
logger: ILogger;
|
|
||||||
|
|
||||||
onTick() {
|
|
||||||
}
|
|
||||||
onComplete?(result: any) {}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -783,6 +783,41 @@ export class ProductService {
|
||||||
return await this.getProductComponents(productId);
|
return await this.getProductComponents(productId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 站点SKU绑定:覆盖式绑定一组站点SKU到产品
|
||||||
|
async bindSiteSkus(productId: number, codes: string[]): Promise<ProductSiteSku[]> {
|
||||||
|
const product = await this.productModel.findOne({ where: { id: productId } });
|
||||||
|
if (!product) throw new Error(`产品 ID ${productId} 不存在`);
|
||||||
|
const normalized = (codes || [])
|
||||||
|
.map(c => String(c).trim())
|
||||||
|
.filter(c => c.length > 0);
|
||||||
|
await this.productSiteSkuModel.delete({ productId });
|
||||||
|
if (normalized.length === 0) return [];
|
||||||
|
const entities = normalized.map(code => {
|
||||||
|
const e = new ProductSiteSku();
|
||||||
|
e.productId = productId;
|
||||||
|
e.code = code;
|
||||||
|
return e;
|
||||||
|
});
|
||||||
|
return await this.productSiteSkuModel.save(entities);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 站点SKU绑定:按单个 code 绑定到指定产品(若已有则更新归属)
|
||||||
|
async bindProductBySiteSku(code: string, productId: number): Promise<ProductSiteSku> {
|
||||||
|
const product = await this.productModel.findOne({ where: { id: productId } });
|
||||||
|
if (!product) throw new Error(`产品 ID ${productId} 不存在`);
|
||||||
|
const skuCode = String(code || '').trim();
|
||||||
|
if (!skuCode) throw new Error('站点SKU不能为空');
|
||||||
|
const existing = await this.productSiteSkuModel.findOne({ where: { code: skuCode } });
|
||||||
|
if (existing) {
|
||||||
|
existing.productId = productId;
|
||||||
|
return await this.productSiteSkuModel.save(existing);
|
||||||
|
}
|
||||||
|
const e = new ProductSiteSku();
|
||||||
|
e.productId = productId;
|
||||||
|
e.code = skuCode;
|
||||||
|
return await this.productSiteSkuModel.save(e);
|
||||||
|
}
|
||||||
|
|
||||||
// 重复定义的 getProductList 已合并到前面的实现(移除重复)
|
// 重复定义的 getProductList 已合并到前面的实现(移除重复)
|
||||||
|
|
||||||
async updatenameCn(id: number, nameCn: string): Promise<Product> {
|
async updatenameCn(id: number, nameCn: string): Promise<Product> {
|
||||||
|
|
@ -804,18 +839,7 @@ export class ProductService {
|
||||||
throw new Error(`产品 ID ${id} 不存在`);
|
throw new Error(`产品 ID ${id} 不存在`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 查询 wp_product 表中是否存在与该 SKU 关联的产品
|
// 不再阻塞于远端站点商品/变体的存在,删除仅按本地引用保护
|
||||||
const wpProduct = await this.wpProductModel.findOne({ where: { sku: product.sku } });
|
|
||||||
if (wpProduct) {
|
|
||||||
throw new Error('无法删除,请先删除关联的WP产品');
|
|
||||||
}
|
|
||||||
|
|
||||||
const variation = await this.variationModel.findOne({ where: { sku: product.sku } });
|
|
||||||
|
|
||||||
if (variation) {
|
|
||||||
console.log(variation);
|
|
||||||
throw new Error('无法删除,请先删除关联的WP变体');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 删除产品
|
// 删除产品
|
||||||
const result = await this.productModel.delete(id);
|
const result = await this.productModel.delete(id);
|
||||||
|
|
|
||||||
|
|
@ -82,7 +82,8 @@ export class UserService {
|
||||||
}
|
}
|
||||||
|
|
||||||
// 新增用户(支持可选备注)
|
// 新增用户(支持可选备注)
|
||||||
async addUser(username: string, password: string, remark?: string) {
|
async addUser(username: string, password: string, remark?: string, email?: string) {
|
||||||
|
// 条件判断 检查用户名是否已存在
|
||||||
const existingUser = await this.userModel.findOne({
|
const existingUser = await this.userModel.findOne({
|
||||||
where: { username },
|
where: { username },
|
||||||
});
|
});
|
||||||
|
|
@ -90,9 +91,17 @@ export class UserService {
|
||||||
throw new Error('用户已存在');
|
throw new Error('用户已存在');
|
||||||
}
|
}
|
||||||
const hashedPassword = await bcrypt.hash(password, 10);
|
const hashedPassword = await bcrypt.hash(password, 10);
|
||||||
|
// 条件判断 若提供邮箱则校验唯一性并赋值
|
||||||
|
if (email) {
|
||||||
|
const existingEmail = await this.userModel.findOne({ where: { email } });
|
||||||
|
if (existingEmail) {
|
||||||
|
throw new Error('邮箱已存在');
|
||||||
|
}
|
||||||
|
}
|
||||||
const user = this.userModel.create({
|
const user = this.userModel.create({
|
||||||
username,
|
username,
|
||||||
password: hashedPassword,
|
password: hashedPassword,
|
||||||
|
...(email ? { email } : {}),
|
||||||
// 备注字段赋值(若提供)
|
// 备注字段赋值(若提供)
|
||||||
...(remark ? { remark } : {}),
|
...(remark ? { remark } : {}),
|
||||||
});
|
});
|
||||||
|
|
@ -106,6 +115,7 @@ export class UserService {
|
||||||
filters: {
|
filters: {
|
||||||
remark?: string;
|
remark?: string;
|
||||||
username?: string;
|
username?: string;
|
||||||
|
email?: string;
|
||||||
isActive?: boolean;
|
isActive?: boolean;
|
||||||
isSuper?: boolean;
|
isSuper?: boolean;
|
||||||
isAdmin?: boolean;
|
isAdmin?: boolean;
|
||||||
|
|
@ -118,12 +128,15 @@ export class UserService {
|
||||||
// 条件判断:构造 where 条件
|
// 条件判断:构造 where 条件
|
||||||
const where: Record<string, any> = {};
|
const where: Record<string, any> = {};
|
||||||
if (filters.username) where.username = Like(`%${filters.username}%`); // 用户名精确匹配(如需模糊可改为 Like)
|
if (filters.username) where.username = Like(`%${filters.username}%`); // 用户名精确匹配(如需模糊可改为 Like)
|
||||||
|
// 条件判断 邮箱模糊搜索
|
||||||
|
if (filters.email) where.email = Like(`%${filters.email}%`);
|
||||||
if (typeof filters.isActive === 'boolean') where.isActive = filters.isActive; // 按启用状态过滤
|
if (typeof filters.isActive === 'boolean') where.isActive = filters.isActive; // 按启用状态过滤
|
||||||
if (typeof filters.isSuper === 'boolean') where.isSuper = filters.isSuper; // 按超管过滤
|
if (typeof filters.isSuper === 'boolean') where.isSuper = filters.isSuper; // 按超管过滤
|
||||||
if (typeof filters.isAdmin === 'boolean') where.isAdmin = filters.isAdmin; // 按管理员过滤
|
if (typeof filters.isAdmin === 'boolean') where.isAdmin = filters.isAdmin; // 按管理员过滤
|
||||||
if (filters.remark) where.remark = Like(`%${filters.remark}%`); // 备注模糊搜索
|
if (filters.remark) where.remark = Like(`%${filters.remark}%`); // 备注模糊搜索
|
||||||
|
|
||||||
const validSortFields = ['id', 'username', 'isActive', 'isSuper', 'isAdmin', 'remark'];
|
// 条件判断 支持邮箱排序字段
|
||||||
|
const validSortFields = ['id', 'username', 'email', 'isActive', 'isSuper', 'isAdmin', 'remark'];
|
||||||
const sortField = validSortFields.includes(sorter.field) ? sorter.field : 'id';
|
const sortField = validSortFields.includes(sorter.field) ? sorter.field : 'id';
|
||||||
const sortOrder = sorter.order === 'ASC' ? 'ASC' : 'DESC';
|
const sortOrder = sorter.order === 'ASC' ? 'ASC' : 'DESC';
|
||||||
|
|
||||||
|
|
@ -151,6 +164,7 @@ export class UserService {
|
||||||
payload: {
|
payload: {
|
||||||
username?: string;
|
username?: string;
|
||||||
password?: string;
|
password?: string;
|
||||||
|
email?: string;
|
||||||
isSuper?: boolean;
|
isSuper?: boolean;
|
||||||
isAdmin?: boolean;
|
isAdmin?: boolean;
|
||||||
permissions?: string[];
|
permissions?: string[];
|
||||||
|
|
@ -175,6 +189,13 @@ export class UserService {
|
||||||
user.password = await bcrypt.hash(payload.password, 10);
|
user.password = await bcrypt.hash(payload.password, 10);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 条件判断 若提供新邮箱且与原邮箱不同,进行唯一性校验
|
||||||
|
if (payload.email && payload.email !== user.email) {
|
||||||
|
const existEmail = await this.userModel.findOne({ where: { email: payload.email } });
|
||||||
|
if (existEmail) throw new Error('邮箱已存在');
|
||||||
|
user.email = payload.email;
|
||||||
|
}
|
||||||
|
|
||||||
// 条件判断:更新布尔与权限字段(若提供则覆盖)
|
// 条件判断:更新布尔与权限字段(若提供则覆盖)
|
||||||
if (typeof payload.isSuper === 'boolean') user.isSuper = payload.isSuper;
|
if (typeof payload.isSuper === 'boolean') user.isSuper = payload.isSuper;
|
||||||
if (typeof payload.isAdmin === 'boolean') user.isAdmin = payload.isAdmin;
|
if (typeof payload.isAdmin === 'boolean') user.isAdmin = payload.isAdmin;
|
||||||
|
|
|
||||||
|
|
@ -168,6 +168,16 @@ export class WPService implements IPlatformService {
|
||||||
return await this.sdkGetPage<WpProduct>(api, 'products', { page, per_page: pageSize });
|
return await this.sdkGetPage<WpProduct>(api, 'products', { page, per_page: pageSize });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// 导出 WooCommerce 产品为特殊CSV(平台特性)
|
||||||
|
async exportProductsCsvSpecial(site: any, page: number = 1, pageSize: number = 100): Promise<string> {
|
||||||
|
const list = await this.getProducts(site, page, pageSize);
|
||||||
|
const header = ['id','name','type','status','sku','regular_price','sale_price','stock_status','stock_quantity'];
|
||||||
|
const rows = (list.items || []).map((p: any) => [p.id,p.name,p.type,p.status,p.sku,p.regular_price,p.sale_price,p.stock_status,p.stock_quantity]);
|
||||||
|
const csv = [header.join(','), ...rows.map(r => r.map(v => String(v ?? '')).join(','))].join('\n');
|
||||||
|
return csv;
|
||||||
|
}
|
||||||
|
|
||||||
async getVariations(site: any, productId: number, page: number = 1, pageSize: number = 100): Promise<any> {
|
async getVariations(site: any, productId: number, page: number = 1, pageSize: number = 100): Promise<any> {
|
||||||
const api = this.createApi(site, 'wc/v3');
|
const api = this.createApi(site, 'wc/v3');
|
||||||
return await this.sdkGetPage<Variation>(api, `products/${productId}/variations`, { page, per_page: pageSize });
|
return await this.sdkGetPage<Variation>(api, `products/${productId}/variations`, { page, per_page: pageSize });
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue