feat: 初始化项目结构并添加核心功能模块
- 新增文档模板和导航结构 - 实现服务器基础API路由和控制器 - 添加扩展插件配置和前端框架 - 引入多租户和权限管理模块 - 集成日志和数据库配置 - 添加核心业务模型和类型定义
This commit is contained in:
483
server/src/services/ProductService.ts
Normal file
483
server/src/services/ProductService.ts
Normal file
@@ -0,0 +1,483 @@
|
||||
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<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;
|
||||
}
|
||||
|
||||
/**
|
||||
* [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({
|
||||
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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user