Files
makemd/server/src/services/ProductService.ts

512 lines
18 KiB
TypeScript
Raw Normal View History

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 { InventoryAgingService } from './InventoryAgingService';
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 InventoryAgingService.analyzeAging(tenantId);
const suggestions = info.filter(item => item.skuId === sku.skuId).map(item => ({
...item,
clearance: item.suggestedAction
}));
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(tenantId, id.toString(), 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<Product> = {
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<Product | null> {
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<Product>): Promise<number> {
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;
}
/**
*
*/
static async createProduct(params: {
tenantId: string;
platform: string;
title: string;
description: string;
price: number;
images: string;
status: string;
}): Promise<string> {
const { tenantId, platform, title, description, price, images, status } = params;
const productData: Partial<Product> = {
platform,
title,
description,
price,
images: JSON.parse(images),
status
};
const id = await this.create(tenantId, productData);
return id.toString();
}
/**
* [CORE_DEV_08] & [CORE_DEV_11] CDC
*/
static async update(tenantId: string, id: number, product: Partial<Product>): Promise<void> {
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<void> {
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<Product[]> {
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('STOCK_UPDATE', {
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<Product>): 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;
}
}