diff --git a/src/adapter/shopyy.adapter.ts b/src/adapter/shopyy.adapter.ts index c8a3b1b..dc820d4 100644 --- a/src/adapter/shopyy.adapter.ts +++ b/src/adapter/shopyy.adapter.ts @@ -248,6 +248,12 @@ export class ShopyyAdapter implements ISiteAdapter { return true; } + async batchProcessProducts( + data: { create?: any[]; update?: any[]; delete?: Array } + ): Promise { + return await this.shopyyService.batchProcessProducts(this.site, data); + } + async getOrders( params: UnifiedSearchParamsDTO ): Promise> { diff --git a/src/adapter/woocommerce.adapter.ts b/src/adapter/woocommerce.adapter.ts index 2b38e70..1e9d00a 100644 --- a/src/adapter/woocommerce.adapter.ts +++ b/src/adapter/woocommerce.adapter.ts @@ -169,6 +169,12 @@ export class WooCommerceAdapter implements ISiteAdapter { } } + async batchProcessProducts( + data: { create?: any[]; update?: any[]; delete?: Array } + ): Promise { + return await this.wpService.batchProcessProducts(this.site, data); + } + async getOrders( params: UnifiedSearchParamsDTO ): Promise> { diff --git a/src/controller/customer.controller.ts b/src/controller/customer.controller.ts index f96a30c..9225aa5 100644 --- a/src/controller/customer.controller.ts +++ b/src/controller/customer.controller.ts @@ -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); diff --git a/src/controller/product.controller.ts b/src/controller/product.controller.ts index 6ba487a..2cb008f 100644 --- a/src/controller/product.controller.ts +++ b/src/controller/product.controller.ts @@ -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) { diff --git a/src/controller/site-api.controller.ts b/src/controller/site-api.controller.ts index e05ec7d..a0e91bc 100644 --- a/src/controller/site-api.controller.ts +++ b/src/controller/site-api.controller.ts @@ -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 } + ) { + 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 = []; + 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 } + ) { + this.logger.info(`[Site API] 批量处理订单开始, siteId: ${siteId}`); + try { + const adapter = await this.siteApiService.getAdapter(siteId); + const created: any[] = []; + const updated: any[] = []; + const deleted: Array = []; + 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 } + ) { + this.logger.info(`[Site API] 批量处理媒体开始, siteId: ${siteId}`); + try { + const adapter = await this.siteApiService.getAdapter(siteId); + const updated: any[] = []; + const deleted: Array = []; + 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 } + ) { + this.logger.info(`[Site API] 批量处理客户开始, siteId: ${siteId}`); + try { + const adapter = await this.siteApiService.getAdapter(siteId); + const created: any[] = []; + const updated: any[] = []; + const deleted: Array = []; + 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); + } + } } diff --git a/src/controller/user.controller.ts b/src/controller/user.controller.ts index 2d8f47f..711f1f2 100644 --- a/src/controller/user.controller.ts +++ b/src/controller/user.controller.ts @@ -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 { diff --git a/src/controller/webhook.controller.ts b/src/controller/webhook.controller.ts index a9dca7d..c4398b0 100644 --- a/src/controller/webhook.controller.ts +++ b/src/controller/webhook.controller.ts @@ -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': diff --git a/src/controller/wp_product.controller.ts b/src/controller/wp_product.controller.ts index 254db29..327c6a2 100644 --- a/src/controller/wp_product.controller.ts +++ b/src/controller/wp_product.controller.ts @@ -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({ diff --git a/src/entity/user.entity.ts b/src/entity/user.entity.ts index 9387610..3b4045f 100644 --- a/src/entity/user.entity.ts +++ b/src/entity/user.entity.ts @@ -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; // 超级管理员 diff --git a/src/interface/site-adapter.interface.ts b/src/interface/site-adapter.interface.ts index 9ace1cc..caa2247 100644 --- a/src/interface/site-adapter.interface.ts +++ b/src/interface/site-adapter.interface.ts @@ -69,13 +69,19 @@ export interface ISiteAdapter { */ deleteProduct(id: string | number): Promise; + batchProcessProducts?(data: { create?: any[]; update?: any[]; delete?: Array }): Promise; + createOrder(data: Partial): Promise; updateOrder(id: string | number, data: Partial): Promise; deleteOrder(id: string | number): Promise; + batchProcessOrders?(data: { create?: any[]; update?: any[]; delete?: Array }): Promise; + getCustomers(params: UnifiedSearchParamsDTO): Promise>; getCustomer(id: string | number): Promise; createCustomer(data: Partial): Promise; updateCustomer(id: string | number, data: Partial): Promise; deleteCustomer(id: string | number): Promise; + + batchProcessCustomers?(data: { create?: any[]; update?: any[]; delete?: Array }): Promise; } diff --git a/src/job/sync_products.job.ts b/src/job/sync_products.job.ts index fb9b229..d4f4e3e 100644 --- a/src/job/sync_products.job.ts +++ b/src/job/sync_products.job.ts @@ -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 {} diff --git a/src/service/product.service.ts b/src/service/product.service.ts index e54ee29..2a7cef7 100644 --- a/src/service/product.service.ts +++ b/src/service/product.service.ts @@ -783,6 +783,41 @@ export class ProductService { return await this.getProductComponents(productId); } + // 站点SKU绑定:覆盖式绑定一组站点SKU到产品 + async bindSiteSkus(productId: number, codes: string[]): Promise { + 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 { + 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 { @@ -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); diff --git a/src/service/user.service.ts b/src/service/user.service.ts index 676bf9c..7303243 100644 --- a/src/service/user.service.ts +++ b/src/service/user.service.ts @@ -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 = {}; 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; diff --git a/src/service/wp.service.ts b/src/service/wp.service.ts index 07d14c8..e88c501 100644 --- a/src/service/wp.service.ts +++ b/src/service/wp.service.ts @@ -168,6 +168,16 @@ export class WPService implements IPlatformService { return await this.sdkGetPage(api, 'products', { page, per_page: pageSize }); } + + // 导出 WooCommerce 产品为特殊CSV(平台特性) + async exportProductsCsvSpecial(site: any, page: number = 1, pageSize: number = 100): Promise { + 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 { const api = this.createApi(site, 'wc/v3'); return await this.sdkGetPage(api, `products/${productId}/variations`, { page, per_page: pageSize });