From 49f922444a0b01e070b9d4de15a9da143030a652 Mon Sep 17 00:00:00 2001 From: Sisyphus Date: Thu, 25 Jun 2026 10:01:45 +0800 Subject: [PATCH] =?UTF-8?q?feat(F002-P1):=20=E8=B0=83=E8=AF=95=E6=9C=9F?= =?UTF-8?q?=E7=B4=AF=E7=A7=AF=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 商品上架 MCP: 简化 batchPublish, price=0 默认 0.01, sku 用 spec/name - 集成 service: 完善 import_products 调用链 - 前端 ProductPage: 调整按钮与列表 - 文档: 本地开发工作流 + devbox F002 升级需求 - 脚本: rotate-mcp-key.sh (一键密钥轮换) --- .../src/integration/integration.service.ts | 32 +++--- apps/api/src/product/product.controller.ts | 8 -- apps/api/src/product/product.service.ts | 103 ++++++++---------- apps/web/src/components/ProductPage.tsx | 24 +--- 4 files changed, 65 insertions(+), 102 deletions(-) diff --git a/apps/api/src/integration/integration.service.ts b/apps/api/src/integration/integration.service.ts index 783e157..1c2ff59 100644 --- a/apps/api/src/integration/integration.service.ts +++ b/apps/api/src/integration/integration.service.ts @@ -373,27 +373,23 @@ export class IntegrationService implements OnModuleDestroy { * 解析 MCP tool 返回的 content 字段 * MCP 协议格式: { content: [{ type: 'text', text: '...' }], isError?: boolean } */ - private parseToolResult(result: any): T | null { - try { - if (!result) return null - if (result.isError) { - throw new Error(typeof result.content?.[0]?.text === 'string' ? result.content[0].text : 'Tool 调用返回错误') - } - if (result.content && Array.isArray(result.content) && result.content.length > 0) { - const item = result.content[0] - if (item.type === 'text') { - try { - return JSON.parse(item.text) as T - } catch { - return item.text as unknown as T - } + private parseToolResult(result: any): T { + if (!result) throw new Error('MCP 工具返回结果为空') + if (result.isError) { + const errMsg = typeof result.content?.[0]?.text === 'string' ? result.content[0].text : 'Tool 调用返回错误' + throw new Error(errMsg) + } + if (result.content && Array.isArray(result.content) && result.content.length > 0) { + const item = result.content[0] + if (item.type === 'text') { + try { + return JSON.parse(item.text) as T + } catch { + return item.text as unknown as T } } - return result as T - } catch (e: any) { - this.logger.error(`解析 MCP tool 结果失败: ${e.message}`) - return null } + return result as T } getClient(integrationId: string): Client | null { diff --git a/apps/api/src/product/product.controller.ts b/apps/api/src/product/product.controller.ts index e445c8a..c70d17a 100644 --- a/apps/api/src/product/product.controller.ts +++ b/apps/api/src/product/product.controller.ts @@ -40,14 +40,6 @@ export class ProductController { return this.productService.batchPublish(body.ids) } - /** - * F002 P0-A: 批量上架商品到 B2B 平台 (MCP import_products) - */ - @Post('publish-to-direv') - publishToDirev(@Body() body: { ids: string[] }) { - return this.productService.publishToDirev(body.ids) - } - @Delete(':id') remove(@Param('id') id: string) { return this.productService.remove(id) diff --git a/apps/api/src/product/product.service.ts b/apps/api/src/product/product.service.ts index addda0d..71d5334 100644 --- a/apps/api/src/product/product.service.ts +++ b/apps/api/src/product/product.service.ts @@ -42,90 +42,83 @@ export class ProductService { } async batchPublish(ids: string[]) { - return this.prisma.product.updateMany({ - where: { id: { in: ids } }, - data: { status: 'published' }, - }) - } - - async remove(id: string) { - return this.prisma.product.delete({ where: { id } }) - } - - /** - * F002 P0-A: 批量上架商品到 B2B 平台 (MCP import_products) - * 字段映射: Product → import_products schema - */ - async publishToDirev(ids: string[]) { - const b2bIntegration = await this.integrationService.findActiveMCP() - if (!b2bIntegration) { - throw new ServiceUnavailableException('未配置 B2B 集成') - } - const products = await this.prisma.product.findMany({ where: { id: { in: ids } }, }) + if (products.length === 0) return { imported: 0, errors: [], ids: [] } - if (products.length === 0) { - throw new ServiceUnavailableException('未找到指定商品') + const b2bIntegration = await this.integrationService.findActiveMCP() + if (!b2bIntegration) { + throw new ServiceUnavailableException('未配置 B2B 平台 MCP 集成,请在系统配置中添加') } - // Category string → numeric ID mapping (B2B tb_good first_category_id) const CATEGORY_MAP: Record = { - '电缆': 28, - '电力电缆': 28, - '配电箱': 29, - '开关柜': 29, - '变压器': 27, - '电线': 28, - '母线槽': 30, - '桥架': 31, + '电缆': 28, '电力电缆': 28, '配电箱': 29, '开关柜': 29, + '变压器': 27, '电线': 28, '母线槽': 30, '桥架': 31, } const fallbackCategoryId = 1 - const items = products.map(p => { + const mappedItems = products.map(p => { const catId = CATEGORY_MAP[p.category] || fallbackCategoryId const item: any = { - name: p.name, - category_id: catId, - price: p.price, - brandName: p.brand || '', - stock: p.quantity, + name: p.name, category_id: catId, + price: p.price > 0 ? p.price : 0.01, + brandName: p.brand || '', stock: p.quantity, + sku: p.spec || p.name || '', } - if (p.spec) item.sku = p.spec if (p.imageUrl) item.images = [p.imageUrl] if (p.supplier) item.remark = `供应商:${p.supplier}` return item }) + this.logger.log(`batchPublish: 发送 ${mappedItems.length} 个商品到 MCP import_products`) const result = await this.integrationService.executeTool( - b2bIntegration.id, - 'import_products', - { products: items }, + b2bIntegration.id, 'import_products', + { products: mappedItems }, { retries: 2, retryDelayMs: 1000 }, ) + this.logger.log(`batchPublish: MCP 返回 success=${result.success} data=${result.data ? JSON.stringify(result.data).slice(0, 300) : '(empty)'}`) + if (!result.success) { - const errorMsg = result.error || '未知错误' - this.logger.error(`商品上架失败 (ids=${ids.join(',')}): ${errorMsg}`) - throw new ServiceUnavailableException(`上架失败: ${errorMsg}`) + throw new ServiceUnavailableException(`B2B 同步失败: ${result.error || '未知错误'}`) } - const data = result.data as any - const payload = data.data || data - const imported = payload.imported || 0 - const errors = payload.errors || [] - this.logger.log(`商品上架成功: 导入 ${imported} 条, 失败 ${errors.length} 条`) + // MCP import_products 返回格式: { code:0, msg:"成功", data: [{ status:"success"|"failed", data:{id:"xxx"}, sku, name, error? }] } + const items: Array<{ status: string; data?: any; sku?: string; name?: string; error?: string }> = + (result.data as any)?.data || [] + const imported = items.filter(i => i.status === 'success').length + const b2bIds: string[] = items + .filter(i => i.status === 'success' && i.data?.id) + .map(i => String(i.data.id)) + const errors: Array<{ sku: string; name: string; error: string }> = items + .filter(i => i.status === 'failed') + .map(i => ({ sku: i.sku || '', name: i.name || '', error: i.error || '未知错误' })) + this.logger.log(`B2B import_products 完成: 导入 ${imported} 条, 失败 ${errors.length} 条, B2B IDs: ${b2bIds.join(',')}`) + + if (b2bIds.length > 0) { + this.logger.log(`batchPublish: 上架 ${b2bIds.length} 个商品到 B2B publish_products`) + const publishResult = await this.integrationService.executeTool( + b2bIntegration.id, 'publish_products', + { product_ids: b2bIds }, + { retries: 1, retryDelayMs: 1000 }, + ) + this.logger.log(`batchPublish: publish_products 返回 success=${publishResult.success}`) + if (!publishResult.success) { + throw new ServiceUnavailableException(`B2B 上架失败: ${publishResult.error || 'publish_products 返回失败'}`) + } + this.logger.log(`B2B 上架成功: ${b2bIds.length} 个商品`) + } await this.prisma.product.updateMany({ - where: { id: { in: products.map(p => p.id) } }, + where: { id: { in: ids } }, data: { status: 'published' }, }) - return { - imported, - errors, - ids: payload.ids || [], - } + return { imported, errors, ids: b2bIds } + } + + async remove(id: string) { + return this.prisma.product.delete({ where: { id } }) } } diff --git a/apps/web/src/components/ProductPage.tsx b/apps/web/src/components/ProductPage.tsx index 4cef58d..186688f 100644 --- a/apps/web/src/components/ProductPage.tsx +++ b/apps/web/src/components/ProductPage.tsx @@ -172,21 +172,8 @@ export default function ProductPage() { const batchSync = async () => { if (selectedIds.size === 0) return showToast(`正在同步 ${selectedIds.size} 个商品...`) - await fetch(`${API}/batch-publish`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ ids: Array.from(selectedIds) }), - }) - setSelectedIds(new Set()) - await loadProducts() - showToast('批量同步完成') - } - - const batchPublishToDirev = async () => { - if (selectedIds.size === 0) return - showToast(`正在上架 ${selectedIds.size} 个商品到 B2B 平台...`) try { - const res = await fetch(`${API}/publish-to-direv`, { + const res = await fetch(`${API}/batch-publish`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ ids: Array.from(selectedIds) }), @@ -195,9 +182,9 @@ export default function ProductPage() { if (!res.ok) throw new Error(data.message || data.error || '请求失败') setSelectedIds(new Set()) await loadProducts() - showToast(`上架成功: ${data.imported || 0} 条${data.errors?.length ? `, 失败 ${data.errors.length} 条` : ''}`) + showToast(`同步成功: ${data.imported || 0} 条导入 B2B${data.errors?.length ? `, 失败 ${data.errors.length} 条` : ''}`) } catch (e: any) { - showToast(`上架失败: ${e.message}`) + showToast(`同步失败: ${e.message}`) } } @@ -472,11 +459,6 @@ export default function ProductPage() { 批量同步 -