import axios from 'axios'; import db from '../config/database'; import { CDCPipeline } from '../core/pipeline/CDCPipeline'; import { DomainEventBus } from '../core/runtime/DomainEventBus'; import { MultiTenantCore } from '../domains/Tenant/MultiTenantCore'; import { Product } from '../models/Product'; import { logger } from '../utils/logger'; import { AgingInventoryService } from './AgingInventoryService'; import { AIService } from './AIService'; import { ArbitrageService } from './ArbitrageService'; import { CompetitorService } from './CompetitorService'; import { ImageFingerprintService } from './ImageFingerprintService'; import { InventoryService } from './InventoryService'; import { MarketingService } from './MarketingService'; import { SupplyChainService } from './SupplyChainService'; import { TaxService } from './TaxService'; import { VisionFactoryService } from './VisionFactoryService'; export class ProductService { private static TABLE_NAME = 'cf_product'; /** * 初始化数据库表 */ static async initTable() { const hasProductTable = await db.schema.hasTable(this.TABLE_NAME); if (!hasProductTable) { console.log(`📦 Creating ${this.TABLE_NAME} table...`); await db.schema.createTable(this.TABLE_NAME, (table) => { table.increments('id').primary(); table.string('tenant_id', 50).notNullable().index(); // [CORE_SEC_45] 租户隔离 table.string('platform', 50).notNullable(); table.string('site', 16).defaultTo('GLOBAL'); // [V30.0] 站点维度 table.string('productId', 100).notNullable(); table.text('title'); table.text('originalTitle'); table.decimal('price', 15, 2).defaultTo(0); table.decimal('originalPrice', 15, 2).defaultTo(0); table.string('currency', 10).defaultTo('CNY'); // 物理属性:CM, KG, M3 (符合业务规则) table.decimal('length_cm', 10, 2).defaultTo(0); table.decimal('width_cm', 10, 2).defaultTo(0); table.decimal('height_cm', 10, 2).defaultTo(0); table.decimal('weight_kg', 10, 3).defaultTo(0); table.string('mainImage', 500); table.string('detailUrl', 1000); table.string('shopName', 200); table.string('shopId', 100); table.bigInteger('sales').defaultTo(0); table.double('rating').defaultTo(0); table.text('description'); // 商品详情 table.json('attributes'); table.json('images'); table.json('skus'); table.string('phash', 64); // [CORE_AI_10] 图像指纹 table.string('semantic_hash', 64); table.text('vector_embedding'); table.string('status', 32).defaultTo('DRAFTED'); // DRAFTED, PENDING_REVIEW, APPROVED, REJECTED table.decimal('ai_confidence', 5, 4).defaultTo(1.0); // AI 信心指数 table.timestamps(true, true); // 索引优化 table.index(['tenant_id', 'platform', 'site'], 'idx_prod_geo'); table.unique(['tenant_id', 'platform', 'productId'], 'uk_prod_id'); table.index(['phash'], 'idx_prod_phash'); table.index(['semantic_hash'], 'idx_prod_semantic'); }); console.log(`✅ Table ${this.TABLE_NAME} created`); } else { // 动态列检查 (CORE_SEC_45) const hasTenantId = await db.schema.hasColumn(this.TABLE_NAME, 'tenant_id'); if (!hasTenantId) { await db.schema.alterTable(this.TABLE_NAME, (table) => { table.string('tenant_id', 50).after('id').index(); }); console.log(`✅ Column tenant_id added to ${this.TABLE_NAME}`); } // 动态列检查 (CORE_AI_10) const hasPhash = await db.schema.hasColumn(this.TABLE_NAME, 'phash'); if (!hasPhash) { await db.schema.alterTable(this.TABLE_NAME, (table) => { table.string('phash', 64).after('skus').index(); }); console.log(`✅ Column phash added to ${this.TABLE_NAME}`); } const hasDescription = await db.schema.hasColumn(this.TABLE_NAME, 'description'); if (!hasDescription) { await db.schema.alterTable(this.TABLE_NAME, (table) => { table.text('description').after('rating'); }); console.log(`✅ Column description added to ${this.TABLE_NAME}`); } const hasSemanticHash = await db.schema.hasColumn(this.TABLE_NAME, 'semantic_hash'); if (!hasSemanticHash) { await db.schema.alterTable(this.TABLE_NAME, (table) => { table.string('semantic_hash', 64).after('phash').index(); }); console.log(`✅ Column semantic_hash added to ${this.TABLE_NAME}`); } const hasVectorEmbedding = await db.schema.hasColumn(this.TABLE_NAME, 'vector_embedding'); if (!hasVectorEmbedding) { await db.schema.alterTable(this.TABLE_NAME, (table) => { table.text('vector_embedding').after('semantic_hash'); }); console.log(`✅ Column vector_embedding added to ${this.TABLE_NAME}`); } const hasStatus = await db.schema.hasColumn(this.TABLE_NAME, 'status'); if (!hasStatus) { await db.schema.alterTable(this.TABLE_NAME, (table) => { table.string('status', 32).after('vector_embedding').defaultTo('DRAFTED'); }); console.log(`✅ Column status added to ${this.TABLE_NAME}`); } const hasAiConfidence = await db.schema.hasColumn(this.TABLE_NAME, 'ai_confidence'); if (!hasAiConfidence) { await db.schema.alterTable(this.TABLE_NAME, (table) => { table.decimal('ai_confidence', 5, 4).after('status').defaultTo(1.0); }); console.log(`✅ Column ai_confidence added to ${this.TABLE_NAME}`); } } } /** * [BIZ_EXT_11] 智能库存预测分析 */ static async predictProductDemand(tenantId: string, id: number) { const product = await this.getById(tenantId, id); if (!product || !product.skus) return null; const predictions = []; for (const sku of product.skus) { const result = await InventoryService.predictSKUDemand(id.toString(), sku.skuId); predictions.push(result); } return predictions; } /** * [BIZ_EXT_12] 执行流失营销挽留任务 */ static async runAbandonedRecoveryTask() { return await MarketingService.processAbandonedCarts(); } /** * [CORE_DEV_04] 抓取并生成图像指纹 */ static async collectAndFingerprint(tenantId: string, id: number) { const product = await this.getById(tenantId, id); if (!product || !product.mainImage) return; try { const response = await axios.get(product.mainImage, { responseType: 'arraybuffer' }); const buffer = Buffer.from(response.data, 'binary'); const hash = await ImageFingerprintService.generatePHash(buffer); // 更新产品图像指纹 (CORE_DEV_04) await this.update(tenantId, id, { phash: hash } as any); return hash; } catch (err: any) { console.error(`[ProductService] Fingerprinting failed: ${err.message}`); } } /** * [BIZ_EXT_05] 库存周转分析与清仓建议 */ static async analyzeInventoryAging(tenantId: string, id: number) { const product = await this.getById(tenantId, id); if (!product || !product.skus) return null; const agingData = []; for (const sku of product.skus) { const info = await AgingInventoryService.analyzeSKUAging(id.toString(), sku.skuId); const suggestions = info.map(item => ({ ...item, clearance: AgingInventoryService.getClearanceSuggestion(item.ageDays) })); agingData.push({ skuId: sku.skuId, agingInfo: suggestions }); } return agingData; } /** * [BIZ_EXT_08] 全球税务合规引擎 */ static async calculateProductTax(tenantId: string, id: number, countryCode: string, isB2B: boolean = false) { const product = await this.getById(tenantId, id); if (!product || !product.costPrice) return null; return await TaxService.calculateTax({ countryCode, baseAmount: product.costPrice, currency: 'USD', isB2B, iossNumber: isB2B ? 'IOSS-TEST-123' : undefined }); } /** * [CORE_AI_15] 视觉/语义复合检索 * @description 通过视觉哈希、语义哈希或向量相似度寻找同款 */ static async findByFingerprint(tenantId: string, params: { phash?: string; semanticHash?: string; vectorEmbedding?: number[]; threshold?: number; }) { const { phash, semanticHash, vectorEmbedding, threshold = 0.9 } = params; const query = MultiTenantCore.from(tenantId, this.TABLE_NAME); // 1. 优先尝试精确哈希匹配 (快速路径) if (phash) { const match = await query.clone().where({ phash }).first(); if (match) return [this.parseProduct(match)]; } if (semanticHash) { const match = await query.clone().where({ semantic_hash: semanticHash }).first(); if (match) return [this.parseProduct(match)]; } // 2. 向量相似度检索 (全量搜索,在大规模场景下应使用专门的向量库) if (vectorEmbedding) { const products = await query.clone().whereNotNull('vector_embedding').select('*'); const results = products .map((p: any) => { const pEmbedding = JSON.parse(p.vector_embedding); const similarity = this.cosineSimilarity(vectorEmbedding, pEmbedding); return { ...p, similarity }; }) .filter((p: any) => p.similarity >= threshold) .sort((a: any, b: any) => b.similarity - a.similarity); return results; } return []; } private static cosineSimilarity(vecA: number[], vecB: number[]): number { let dotProduct = 0; let normA = 0; let normB = 0; for (let i = 0; i < vecA.length; i++) { dotProduct += vecA[i] * vecB[i]; normA += vecA[i] * vecA[i]; normB += vecB[i] * vecB[i]; } return dotProduct / (Math.sqrt(normA) * Math.sqrt(normB)); } /** * [BIZ_EXT_09] 全球同行嗅探器 */ static async sniffCompetitors(tenantId: string, id: number) { const product = await this.getById(tenantId, id); if (!product || !product.images || product.images.length === 0) return null; const results = []; for (const imageUrl of product.images) { // 假设 imageUrl 就是指纹,或者先转为指纹 const res = await CompetitorService.findCrossPlatformMatches(imageUrl); results.push(...res); } return results; } /** * [BIZ_EXT_10] 供应链全链路溯源 */ static async trackSupplyChain(tenantId: string, id: number) { const product = await this.getById(tenantId, id); if (!product) return null; return await SupplyChainService.traceSourceFactory(product.mainImage); } /** * [BIZ_EXT_07] 分析选品利差 */ static async analyzeProductArbitrage(tenantId: string, id: number) { const product = await this.getById(tenantId, id); if (!product || !product.costPrice) return null; return await ArbitrageService.analyzeArbitrage(id.toString(), product.costPrice); } /** * [BIZ_DEV_02] 侵权合规性审计 */ static async auditProductIP(tenantId: string, id: number) { const product = await this.getById(tenantId, id); if (!product) return null; const result = await AIService.checkIPRisk({ title: product.title, description: product.description, imageUrls: product.images || [] }); if (result.isRisky) { await this.update(tenantId, id, { status: 'rejected' } as any); // 假设 status 是 cf_product 字段 } return result; } /** * [BIZ_PROD_12] AI 商品全自动“清洗”与本地化 (Washer) * @description 过滤非法字符/Logo,重写地道 Listing,并生成目标 market 营销图 */ static async washAndLocalize(tenantId: string, id: number, targetMarket: string, targetLang: string = 'en') { const product = await this.getById(tenantId, id); if (!product) throw new Error('Product not found'); logger.info(`[ProductService] Starting "Washer" for Product ${id} (${targetMarket})`); // 1. 语义清洗与本地化 (AIService) const localization = await AIService.localizeForIndependentStation({ title: product.title || product.originalTitle || '', description: product.description || '', targetMarket }); // 2. 视觉本地化增强 (VisionFactoryService) let marketingImage = product.mainImage; if (product.mainImage) { marketingImage = await VisionFactoryService.generateMarketingImage({ originalImageUrl: product.mainImage, productTitle: localization.seoTitle, targetMarket, aestheticStyle: targetMarket === 'Middle East' ? 'Gold & Luxury' : 'Minimalist' }); } // 3. 更新产品数据 const updateData: Partial = { title: localization.seoTitle, description: localization.localizedBodyHtml, mainImage: marketingImage, // 将 SEO 标签作为扩展属性存储 (此处模拟) attributes: { ...product.attributes, seo_tags: JSON.stringify(localization.tags), json_ld: localization.jsonLd } }; await this.update(tenantId, id, updateData); return { id, originalTitle: product.title, newTitle: localization.seoTitle, marketingImage, tags: localization.tags }; } static async getAll(tenantId: string, params?: { platform?: string; status?: string }) { const query = MultiTenantCore.from(tenantId, this.TABLE_NAME); if (params?.platform) query.where({ platform: params.platform }); if (params?.status) query.where({ status: params.status }); const products = await query.select('*'); return products.map((p: any) => this.parseProduct(p)); } static async getById(tenantId: string, id: number): Promise { const product = await MultiTenantCore.from(tenantId, this.TABLE_NAME).where({ id }).first(); return product ? this.parseProduct(product) : null; } static async create(tenantId: string, product: Partial): Promise { const data = { ...this.formatProduct(product), tenant_id: tenantId, created_at: new Date(), updated_at: new Date(), }; const [id] = await db(this.TABLE_NAME).insert(data); return id; } /** * [CORE_DEV_08] 集成向量化存储 & [CORE_DEV_11] CDC 拦截 */ static async update(tenantId: string, id: number, product: Partial): Promise { const owned = await MultiTenantCore.ensureOwnership(tenantId, this.TABLE_NAME, id); if (!owned) throw new Error('Unauthorized access to product record'); const data = { ...this.formatProduct(product), updated_at: new Date(), }; await MultiTenantCore.from(tenantId, this.TABLE_NAME).where({ id }).update(data); const updatedProduct = await this.getById(tenantId, id); // 1. 触发 CDC 变更拦截 (CORE_DEV_11) if (updatedProduct) { CDCPipeline.onProductChange(id, 'UPDATE', updatedProduct).catch(err => { logger.error(`[ProductService] CDC intercept failed: ${err.message}`); }); } } static async delete(tenantId: string, id: number): Promise { const owned = await MultiTenantCore.ensureOwnership(tenantId, this.TABLE_NAME, id); if (!owned) throw new Error('Unauthorized access to product record'); await MultiTenantCore.from(tenantId, this.TABLE_NAME).where({ id }).delete(); } static async findByPhash(tenantId: string, phash: string): Promise { const results = await MultiTenantCore.from(tenantId, this.TABLE_NAME).where({ phash }).select('*'); return results.map((p: any) => this.parseProduct(p)); } /** * [BIZ_EXT_01] 模拟商品同步逻辑 */ static async syncProduct(productId: string, tenantId: string) { logger.info(`[ProductService] Syncing product ${productId} for tenant ${tenantId}`); // 实际业务中应对接第三方平台 API 获取最新状态并更新数据库 await new Promise(resolve => setTimeout(resolve, 500)); } /** * 更新商品库存 (BIZ_INV_05) */ static async updateStock(tenantId: string, productId: string, delta: number) { logger.info(`[ProductService] Updating stock for ${productId} by ${delta}`); const product = await db(this.TABLE_NAME).where({ id: productId, tenant_id: tenantId }).first(); if (!product) throw new Error('Product not found'); await db(this.TABLE_NAME).where({ id: productId }).update({ stock: (product.stock || 0) + delta, updated_at: new Date() }); // [BIZ_GOV_20] 发布业务事件到总线,触发自动审计 DomainEventBus.getInstance().publish({ tenantId, module: 'INVENTORY', action: 'STOCK_UPDATE', resourceType: 'PRODUCT', resourceId: productId, data: { delta, newStock: (product.stock || 0) + delta }, traceId: 'stock-update-' + Date.now() }); } private static parseProduct(dbData: any): Product { return { ...dbData, images: typeof dbData.images === 'string' ? JSON.parse(dbData.images) : dbData.images, skus: typeof dbData.skus === 'string' ? JSON.parse(dbData.skus) : dbData.skus, attributes: typeof dbData.attributes === 'string' ? JSON.parse(dbData.attributes) : dbData.attributes, }; } private static formatProduct(product: Partial): any { const formatted: any = { ...product }; if (product.images) formatted.images = JSON.stringify(product.images); if (product.skus) formatted.skus = JSON.stringify(product.skus); if (product.attributes) formatted.attributes = JSON.stringify(product.attributes); delete formatted.id; delete formatted.tenant_id; delete formatted.createdAt; delete formatted.updatedAt; return formatted; } }