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:
tikkhun 2025-12-12 18:40:56 +08:00
parent 87b4039a67
commit 3f3569995d
14 changed files with 674 additions and 191 deletions

View File

@ -248,6 +248,12 @@ export class ShopyyAdapter implements ISiteAdapter {
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(
params: UnifiedSearchParamsDTO
): Promise<UnifiedPaginationDTO<UnifiedOrderDTO>> {

View File

@ -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(
params: UnifiedSearchParamsDTO
): Promise<UnifiedPaginationDTO<UnifiedOrderDTO>> {

View File

@ -1,23 +1,63 @@
import { Controller, Get, Inject, Query } from '@midwayjs/core';
import { WPService } from '../service/wp.service';
import { Controller, Get, Post, Inject, Query, Body } from '@midwayjs/core';
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')
export class CustomerController {
@Inject()
wpService: WPService;
customerService: CustomerService;
@Get('/list')
async list(
@Query('siteId') siteId: number,
@Query('page') page: number = 1,
@Query('pageSize') pageSize: number = 20
) {
@ApiOkResponse({ type: Object })
@Get('/getcustomerlist')
async getCustomerList(@Query() query: QueryCustomerListDTO) {
try {
if (!siteId) {
return errorResponse('siteId is required');
}
const result = await this.wpService.getCustomers(siteId, page, pageSize);
const result = await this.customerService.getCustomerList(query as any);
return successResponse(result);
} catch (error) {
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);
} catch (error) {
return errorResponse(error.message);

View File

@ -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 })
@Del('/:id')
async deleteProduct(@Param('id') id: number) {

View File

@ -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')
@ApiOkResponse({ type: UnifiedProductDTO })
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')
@ApiOkResponse({ type: UnifiedProductDTO })
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')
@ApiOkResponse({ type: UnifiedOrderPaginationDTO })
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')
@ApiOkResponse({ type: UnifiedOrderDTO })
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')
@ApiOkResponse({ type: Boolean })
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')
@ApiOkResponse({ type: Object })
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')
@ApiOkResponse({ type: UnifiedMediaPaginationDTO })
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')
@ApiOkResponse({ type: Boolean })
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')
@ApiOkResponse({ type: UnifiedCustomerPaginationDTO })
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')
@ApiOkResponse({ type: UnifiedCustomerDTO })
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')
@ApiOkResponse({ type: UnifiedCustomerDTO })
async updateCustomer(
@ -434,4 +855,57 @@ export class SiteApiController {
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);
}
}
}

View File

@ -40,11 +40,11 @@ export class UserController {
}
@Post('/add')
async addUser(@Body() body: { username: string; password: string; remark?: string }) {
const { username, password, remark } = body;
async addUser(@Body() body: { username: string; password: string; email?: string; remark?: string }) {
const { username, password, email, remark } = body;
try {
// 新增用户(支持备注)
await this.userService.addUser(username, password, remark);
// 新增用户 支持邮箱与备注
await this.userService.addUser(username, password, remark, email);
return successResponse(true);
} catch (error) {
console.log(error);
@ -60,6 +60,7 @@ export class UserController {
pageSize: number;
remark?: string;
username?: string;
email?: string;
isActive?: string;
isSuper?: string;
isAdmin?: string;
@ -67,7 +68,7 @@ export class UserController {
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');
// 处理排序方向
@ -80,6 +81,7 @@ export class UserController {
{
remark,
username,
email,
isActive: toBool(isActive),
isSuper: toBool(isSuper),
isAdmin: toBool(isAdmin),
@ -112,7 +114,7 @@ export class UserController {
// 更新用户(支持用户名/密码/权限/角色更新)
@Post('/update/:id')
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
) {
try {

View File

@ -9,8 +9,7 @@ import {
} from '@midwayjs/decorator';
import { Context } from '@midwayjs/koa';
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 { OrderService } from '../service/order.service';
@ -18,11 +17,7 @@ import { OrderService } from '../service/order.service';
export class WebhookController {
private secret = 'YOONE24kd$kjcdjflddd';
@Inject()
private readonly wpProductService: WpProductService;
@Inject()
private readonly wpApiService: WPService;
// 平台服务保留按需注入
@Inject()
private readonly orderService: OrderService;
@ -79,32 +74,10 @@ export class WebhookController {
switch (topic) {
case 'product.created':
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;
case 'product.deleted':
await this.wpProductService.delWpProduct(site.id, body.id);
// 不再写入本地,平台事件仅确认接收
break;
case 'order.created':
case 'order.updated':

View File

@ -22,8 +22,7 @@ import {
BatchUpdateTagsDTO,
BatchUpdateProductsDTO,
} from '../dto/wp_product.dto';
import { WPService } from '../service/wp.service';
import { SiteService } from '../service/site.service';
import {
ProductsRes,
} from '../dto/reponse.dto';
@ -34,23 +33,14 @@ export class WpProductController {
@Inject()
private readonly wpProductService: WpProductService;
@Inject()
private readonly wpApiService: WPService;
@Inject()
private readonly siteService: SiteService;
// 平台服务保留按需注入
@ApiOkResponse({
type: BooleanRes,
})
@Del('/:id')
async delete(@Param('id') id: number) {
try {
await this.wpProductService.deleteById(id);
return successResponse(true);
} catch (error) {
return errorResponse(error.message || '删除失败');
}
return errorResponse('接口已废弃,请改用 /site-api/:siteId/products 删除');
}
@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({
type: BooleanRes,
})
@ -132,12 +134,7 @@ export class WpProductController {
})
@Get('/list')
async getWpProducts(@Query() query: QueryWpProductDTO) {
try {
const data = await this.wpProductService.getProductList(query);
return successResponse(data);
} catch (error) {
return errorResponse(error.message);
}
return errorResponse('接口已废弃,请改用 /site-api/:siteId/products 列表');
}
@ApiOkResponse({
@ -169,29 +166,7 @@ export class WpProductController {
@Param('siteId') siteId: number,
@Body() body: any
) {
try {
// 过滤掉前端可能传入的多余字段
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 || '产品创建失败');
}
return errorResponse('接口已废弃,请改用 /site-api/:siteId/products 创建');
}
/**
@ -208,45 +183,7 @@ export class WpProductController {
@Param('productId') productId: string,
@Body() body: UpdateWpProductDTO
) {
try {
// ? 这个是啥意思
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 || '产品更新失败');
}
return errorResponse('接口已废弃,请改用 /site-api/:siteId/products/:id 更新');
}
@ApiOkResponse({
@ -275,37 +212,7 @@ export class WpProductController {
@Param('variationId') variationId: string,
@Body() body: UpdateVariationDTO
) {
try {
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 || '产品变体更新失败');
}
return errorResponse('接口已废弃,请改用 /site-api/:siteId/products/:productId/variations/:variationId 更新');
}
@ApiOkResponse({

View File

@ -20,6 +20,10 @@ export class User {
@Column({ type: 'simple-array', nullable: true })
permissions: string[]; // 自定义权限 (如:['user:add', 'user:edit'])
// 新增邮箱字段,可选且唯一
@Column({ unique: true, nullable: true })
email?: string;
@Column({ default: false })
isSuper: boolean; // 超级管理员

View File

@ -69,13 +69,19 @@ export interface ISiteAdapter {
*/
deleteProduct(id: string | number): Promise<boolean>;
batchProcessProducts?(data: { create?: any[]; update?: any[]; delete?: Array<string | number> }): Promise<any>;
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>;
getCustomers(params: UnifiedSearchParamsDTO): Promise<UnifiedPaginationDTO<UnifiedCustomerDTO>>;
getCustomer(id: string | number): Promise<UnifiedCustomerDTO>;
createCustomer(data: Partial<UnifiedCustomerDTO>): Promise<UnifiedCustomerDTO>;
updateCustomer(id: string | number, data: Partial<UnifiedCustomerDTO>): Promise<UnifiedCustomerDTO>;
deleteCustomer(id: string | number): Promise<boolean>;
batchProcessCustomers?(data: { create?: any[]; update?: any[]; delete?: Array<string | number> }): Promise<any>;
}

View File

@ -1,15 +1 @@
import { FORMAT, ILogger, Logger } from '@midwayjs/core';
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) {}
}
export {}

View File

@ -783,6 +783,41 @@ export class ProductService {
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 已合并到前面的实现(移除重复)
async updatenameCn(id: number, nameCn: string): Promise<Product> {
@ -804,18 +839,7 @@ export class ProductService {
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);

View File

@ -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({
where: { username },
});
@ -90,9 +91,17 @@ export class UserService {
throw new Error('用户已存在');
}
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({
username,
password: hashedPassword,
...(email ? { email } : {}),
// 备注字段赋值(若提供)
...(remark ? { remark } : {}),
});
@ -106,6 +115,7 @@ export class UserService {
filters: {
remark?: string;
username?: string;
email?: string;
isActive?: boolean;
isSuper?: boolean;
isAdmin?: boolean;
@ -118,12 +128,15 @@ export class UserService {
// 条件判断:构造 where 条件
const where: Record<string, any> = {};
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.isSuper === 'boolean') where.isSuper = filters.isSuper; // 按超管过滤
if (typeof filters.isAdmin === 'boolean') where.isAdmin = filters.isAdmin; // 按管理员过滤
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 sortOrder = sorter.order === 'ASC' ? 'ASC' : 'DESC';
@ -151,6 +164,7 @@ export class UserService {
payload: {
username?: string;
password?: string;
email?: string;
isSuper?: boolean;
isAdmin?: boolean;
permissions?: string[];
@ -175,6 +189,13 @@ export class UserService {
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.isAdmin === 'boolean') user.isAdmin = payload.isAdmin;

View File

@ -168,6 +168,16 @@ export class WPService implements IPlatformService {
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> {
const api = this.createApi(site, 'wc/v3');
return await this.sdkGetPage<Variation>(api, `products/${productId}/variations`, { page, per_page: pageSize });