Files
makemd/server/src/services/ProductService.ts
wurenzhi 5cfd0c4c89 feat: 实现服务层核心功能与文档更新
refactor(ProductService): 修复createProduct方法和其他方法错误
fix(InventoryAgingService): 修复AGING_THRESHOLD_DAYS引用问题
fix(InventoryService): 修复predictSKUDemand方法
refactor(ChatBotController): 从tsoa风格改为Express风格
fix(CommandCenterController): 修复类型问题
fix(AdAutoService): 修复stock可能为undefined的问题
docs: 更新SERVICE_MAP、DOMAIN_MODEL等架构文档
chore: 启动前端服务(运行在http://localhost:8000)
2026-03-18 12:35:52 +08:00

512 lines
18 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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;
}
}