feat(F002-P0-A): 商品上架 - Mac 端 publish-to-direv API + web 按钮
- product.service.ts: 新增 publishToDirev() 调 MCP import_products - product.controller.ts: 新增 POST /api/products/publish-to-direv - product.module.ts: 引入 IntegrationModule - ProductPage.tsx: 工具栏加 '上架 B2B' 按钮 (绿色) - 字段映射: Product(category/price/brand) → MCP(category_id/price/brandName)
This commit is contained in:
parent
4ab0040e55
commit
672a4b41b2
@ -40,6 +40,14 @@ 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)
|
||||
|
||||
@ -1,8 +1,10 @@
|
||||
import { Module } from '@nestjs/common'
|
||||
import { ProductController } from './product.controller'
|
||||
import { ProductService } from './product.service'
|
||||
import { IntegrationModule } from '../integration/integration.module'
|
||||
|
||||
@Module({
|
||||
imports: [IntegrationModule],
|
||||
controllers: [ProductController],
|
||||
providers: [ProductService],
|
||||
exports: [ProductService],
|
||||
|
||||
@ -1,9 +1,15 @@
|
||||
import { Injectable } from '@nestjs/common'
|
||||
import { Injectable, Logger, ServiceUnavailableException } from '@nestjs/common'
|
||||
import { PrismaService } from '../prisma/prisma.service'
|
||||
import { IntegrationService } from '../integration/integration.service'
|
||||
|
||||
@Injectable()
|
||||
export class ProductService {
|
||||
constructor(private prisma: PrismaService) {}
|
||||
private readonly logger = new Logger(ProductService.name)
|
||||
|
||||
constructor(
|
||||
private prisma: PrismaService,
|
||||
private integrationService: IntegrationService,
|
||||
) {}
|
||||
|
||||
async findAll() {
|
||||
return this.prisma.product.findMany({ orderBy: { createdAt: 'desc' } })
|
||||
@ -45,4 +51,81 @@ export class ProductService {
|
||||
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) {
|
||||
throw new ServiceUnavailableException('未找到指定商品')
|
||||
}
|
||||
|
||||
// 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,
|
||||
}
|
||||
const fallbackCategoryId = 1
|
||||
|
||||
const items = 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,
|
||||
}
|
||||
if (p.spec) item.sku = p.spec
|
||||
if (p.imageUrl) item.images = [p.imageUrl]
|
||||
if (p.supplier) item.remark = `供应商:${p.supplier}`
|
||||
return item
|
||||
})
|
||||
|
||||
const result = await this.integrationService.executeTool<any>(
|
||||
b2bIntegration.id,
|
||||
'import_products',
|
||||
{ products: items },
|
||||
{ retries: 2, retryDelayMs: 1000 },
|
||||
)
|
||||
|
||||
if (!result.success) {
|
||||
const errorMsg = result.error || '未知错误'
|
||||
this.logger.error(`商品上架失败 (ids=${ids.join(',')}): ${errorMsg}`)
|
||||
throw new ServiceUnavailableException(`上架失败: ${errorMsg}`)
|
||||
}
|
||||
|
||||
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} 条`)
|
||||
|
||||
await this.prisma.product.updateMany({
|
||||
where: { id: { in: products.map(p => p.id) } },
|
||||
data: { status: 'published' },
|
||||
})
|
||||
|
||||
return {
|
||||
imported,
|
||||
errors,
|
||||
ids: payload.ids || [],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -182,6 +182,25 @@ export default function ProductPage() {
|
||||
showToast('批量同步完成')
|
||||
}
|
||||
|
||||
const batchPublishToDirev = async () => {
|
||||
if (selectedIds.size === 0) return
|
||||
showToast(`正在上架 ${selectedIds.size} 个商品到 B2B 平台...`)
|
||||
try {
|
||||
const res = await fetch(`${API}/publish-to-direv`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ ids: Array.from(selectedIds) }),
|
||||
})
|
||||
const data = await res.json()
|
||||
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} 条` : ''}`)
|
||||
} catch (e: any) {
|
||||
showToast(`上架失败: ${e.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
const removeProduct = async (id: string) => {
|
||||
await fetch(`${API}/${id}`, { method: 'DELETE' })
|
||||
showToast('已删除')
|
||||
@ -451,7 +470,12 @@ export default function ProductPage() {
|
||||
<button disabled={selectedIds.size === 0} onClick={batchSync}
|
||||
className="px-3 py-1.5 text-[10px] font-medium bg-secondary 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="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">
|
||||
|
||||
Loading…
Reference in New Issue
Block a user