feat(F002-P1): 调试期累积修复
- 商品上架 MCP: 简化 batchPublish, price=0 默认 0.01, sku 用 spec/name - 集成 service: 完善 import_products 调用链 - 前端 ProductPage: 调整按钮与列表 - 文档: 本地开发工作流 + devbox F002 升级需求 - 脚本: rotate-mcp-key.sh (一键密钥轮换)
This commit is contained in:
parent
672a4b41b2
commit
49f922444a
@ -373,11 +373,11 @@ export class IntegrationService implements OnModuleDestroy {
|
||||
* 解析 MCP tool 返回的 content 字段
|
||||
* MCP 协议格式: { content: [{ type: 'text', text: '...' }], isError?: boolean }
|
||||
*/
|
||||
private parseToolResult<T>(result: any): T | null {
|
||||
try {
|
||||
if (!result) return null
|
||||
private parseToolResult<T>(result: any): T {
|
||||
if (!result) throw new Error('MCP 工具返回结果为空')
|
||||
if (result.isError) {
|
||||
throw new Error(typeof result.content?.[0]?.text === 'string' ? result.content[0].text : 'Tool 调用返回错误')
|
||||
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]
|
||||
@ -390,10 +390,6 @@ export class IntegrationService implements OnModuleDestroy {
|
||||
}
|
||||
}
|
||||
return result as T
|
||||
} catch (e: any) {
|
||||
this.logger.error(`解析 MCP tool 结果失败: ${e.message}`)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
getClient(integrationId: string): Client | null {
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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<string, number> = {
|
||||
'电缆': 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<any>(
|
||||
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<any>(
|
||||
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 } })
|
||||
}
|
||||
}
|
||||
|
||||
@ -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() {
|
||||
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" /></svg>
|
||||
批量同步
|
||||
</button>
|
||||
<button disabled={selectedIds.size === 0} onClick={batchPublishToDirev}
|
||||
className="px-3 py-1.5 text-[10px] font-medium bg-emerald-600 text-white rounded-lg disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-1 hover:opacity-90">
|
||||
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" /></svg>
|
||||
上架 B2B
|
||||
</button>
|
||||
<button disabled={selectedIds.size === 0} onClick={batchGenerate}
|
||||
className="px-3 py-1.5 text-[10px] font-medium bg-white border border-purple-200 text-purple-700 rounded-lg disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-1 hover:bg-purple-50 transition-colors">
|
||||
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" /></svg>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user