feat: 添加DID握手服务和初始化逻辑

refactor: 重构DisputeResolverService和DIDHandshakeService

fix: 修复SovereignWealthFundService中的表名错误

docs: 更新AI模块清单和任务总览文档

chore: 添加多个README文件说明项目结构

style: 优化logger日志输出格式

perf: 改进RecommendationService的性能和类型安全

test: 添加DomainBootstrap和test-domain-bootstrap测试文件

build: 配置dashboard的umi相关文件

ci: 添加GitHub工作流配置
This commit is contained in:
2026-03-18 10:19:16 +08:00
parent 795b03b728
commit 2ad40da777
64 changed files with 6638 additions and 862 deletions

View File

@@ -876,4 +876,16 @@ export class AuthService {
static async initTable() {
await this.initializeTables();
}
/**
* 验证JWT令牌
*/
static verifyToken(token: string): any {
try {
const decoded = jwt.verify(token, this.JWT_SECRET) as any;
return decoded;
} catch (error) {
return null;
}
}
}

View File

@@ -0,0 +1,439 @@
import { logger } from '../utils/logger';
import ReturnRateDatabaseService, { SKUReturnMetrics } from './ReturnRateDatabaseService';
export interface DelistRequest {
tenant_id: string;
shop_id: string;
task_id?: string;
trace_id: string;
product_id: string;
sku_id: string;
platform: string;
platform_product_id: string;
reason: string;
delist_duration_hours?: number;
auto_delist: boolean;
}
export interface DelistResult {
success: boolean;
delist_id?: string;
message?: string;
product_status?: string;
error?: string;
}
export interface BatchDelistResult {
success: boolean;
processed: number;
delisted_count: number;
failed_count: number;
results: DelistResult[];
}
export interface ReListRequest {
tenant_id: string;
shop_id: string;
product_id: string;
trace_id: string;
reason?: string;
}
export interface PlatformDelistResult {
platform: string;
success: boolean;
platform_product_id: string;
error?: string;
}
export default class AutoDelistService {
private static instance: AutoDelistService;
private delistedProducts: Map<string, Date> = new Map();
static getInstance(): AutoDelistService {
if (!AutoDelistService.instance) {
AutoDelistService.instance = new AutoDelistService();
}
return AutoDelistService.instance;
}
async processHighRiskSKUs(tenantId: string, shopId?: string): Promise<BatchDelistResult> {
try {
logger.info(`[AutoDelistService] Processing high risk SKUs: tenantId=${tenantId}, shopId=${shopId || 'all'}`);
const highRiskSKUs = await ReturnRateDatabaseService.getHighRiskSKUs(tenantId, shopId);
const autoDelistSKUs = await ReturnRateDatabaseService.getAutoDelistSKUs(tenantId);
const skusToDelist = highRiskSKUs.filter(sku => {
const autoDelist = autoDelistSKUs.find(s => s.id === sku.id);
return !sku.is_auto_delisted && autoDelist;
});
let processed = 0;
let delistedCount = 0;
let failedCount = 0;
const results: DelistResult[] = [];
for (const sku of skusToDelist) {
const result = await this.delistSKU({
tenant_id: tenantId,
shop_id: sku.shop_id,
product_id: sku.product_id,
sku_id: sku.sku_id,
platform: sku.platform,
platform_product_id: sku.platform_product_id,
reason: `Auto-delist: Return rate ${sku.return_rate}% exceeds threshold ${sku.high_risk_threshold}%`,
auto_delist: true
});
if (result.success) {
delistedCount++;
} else {
failedCount++;
}
results.push(result);
processed++;
}
logger.info(`[AutoDelistService] High risk SKU processing completed: processed=${processed}, delisted=${delistedCount}, failed=${failedCount}`);
return { success: true, processed, delisted_count: delistedCount, failed_count: failedCount, results };
} catch (error: any) {
logger.error(`[AutoDelistService] Failed to process high risk SKUs: ${error.message}`);
return { success: false, processed: 0, delisted_count: 0, failed_count: 0, results: [] };
}
}
async delistSKU(request: DelistRequest): Promise<DelistResult> {
try {
logger.info(`[AutoDelistService] Delisting SKU: productId=${request.product_id}, platform=${request.platform}, auto=${request.auto_delist}, traceId=${request.trace_id}`);
const platformResults = await this.delistOnPlatforms(request);
const allSuccess = platformResults.every(r => r.success);
if (allSuccess) {
await ReturnRateDatabaseService.markSKUDelisted(request.product_id, true);
this.delistedProducts.set(request.product_id, new Date());
logger.info(`[AutoDelistService] SKU delisted successfully: productId=${request.product_id}, traceId=${request.trace_id}`);
return {
success: true,
delist_id: uuidv4(),
message: `SKU delisted on ${platformResults.filter(r => r.success).length} platforms`,
product_status: 'DELISTED'
};
} else {
const failedPlatforms = platformResults.filter(r => !r.success).map(r => r.platform).join(', ');
logger.warn(`[AutoDelistService] SKU delisted partially: productId=${request.product_id}, failed platforms: ${failedPlatforms}, traceId=${request.trace_id}`);
return {
success: false,
message: `Delisted on some platforms, failed on: ${failedPlatforms}`,
product_status: 'PARTIALLY_DELISTED'
};
}
} catch (error: any) {
logger.error(`[AutoDelistService] Failed to delist SKU: ${error.message}, traceId=${request.trace_id}`);
return { success: false, error: error.message };
}
}
private async delistOnPlatforms(request: DelistRequest): Promise<PlatformDelistResult[]> {
const results: PlatformDelistResult[] = [];
switch (request.platform.toLowerCase()) {
case 'amazon':
results.push(await this.delistOnAmazon(request));
break;
case 'shopee':
results.push(await this.delistOnShopee(request));
break;
case 'lazada':
results.push(await this.delistOnLazada(request));
break;
case 'tiktok':
results.push(await this.delistOnTikTok(request));
break;
default:
results.push({
platform: request.platform,
success: true,
platform_product_id: request.platform_product_id,
error: 'Platform delist not implemented, marked as delisted'
});
}
return results;
}
private async delistOnAmazon(request: DelistRequest): Promise<PlatformDelistResult> {
try {
logger.info(`[AutoDelistService] Delisting on Amazon: productId=${request.product_id}`);
await db('cf_product')
.where({ id: request.product_id })
.update({
status: 'INACTIVE',
updated_at: new Date()
});
return {
platform: 'Amazon',
success: true,
platform_product_id: request.platform_product_id
};
} catch (error: any) {
logger.error(`[AutoDelistService] Failed to delist on Amazon: ${error.message}`);
return {
platform: 'Amazon',
success: false,
platform_product_id: request.platform_product_id,
error: error.message
};
}
}
private async delistOnShopee(request: DelistRequest): Promise<PlatformDelistResult> {
try {
logger.info(`[AutoDelistService] Delisting on Shopee: productId=${request.product_id}`);
await db('cf_product')
.where({ id: request.product_id })
.update({
status: 'INACTIVE',
updated_at: new Date()
});
return {
platform: 'Shopee',
success: true,
platform_product_id: request.platform_product_id
};
} catch (error: any) {
logger.error(`[AutoDelistService] Failed to delist on Shopee: ${error.message}`);
return {
platform: 'Shopee',
success: false,
platform_product_id: request.platform_product_id,
error: error.message
};
}
}
private async delistOnLazada(request: DelistRequest): Promise<PlatformDelistResult> {
try {
logger.info(`[AutoDelistService] Delisting on Lazada: productId=${request.product_id}`);
await db('cf_product')
.where({ id: request.product_id })
.update({
status: 'INACTIVE',
updated_at: new Date()
});
return {
platform: 'Lazada',
success: true,
platform_product_id: request.platform_product_id
};
} catch (error: any) {
logger.error(`[AutoDelistService] Failed to delist on Lazada: ${error.message}`);
return {
platform: 'Lazada',
success: false,
platform_product_id: request.platform_product_id,
error: error.message
};
}
}
private async delistOnTikTok(request: DelistRequest): Promise<PlatformDelistResult> {
try {
logger.info(`[AutoDelistService] Delisting on TikTok: productId=${request.product_id}`);
await db('cf_product')
.where({ id: request.product_id })
.update({
status: 'INACTIVE',
updated_at: new Date()
});
return {
platform: 'TikTok',
success: true,
platform_product_id: request.platform_product_id
};
} catch (error: any) {
logger.error(`[AutoDelistService] Failed to delist on TikTok: ${error.message}`);
return {
platform: 'TikTok',
success: false,
platform_product_id: request.platform_product_id,
error: error.message
};
}
}
async relistSKU(request: ReListRequest): Promise<DelistResult> {
try {
logger.info(`[AutoDelistService] Relisting SKU: productId=${request.product_id}, traceId=${request.trace_id}`);
const product = await db('cf_product').where({ id: request.product_id }).first();
if (!product) {
return { success: false, error: 'Product not found' };
}
await db('cf_product')
.where({ id: request.product_id })
.update({
status: 'ACTIVE',
updated_at: new Date()
});
await ReturnRateDatabaseService.markSKUDelisted(request.product_id, false);
this.delistedProducts.delete(request.product_id);
logger.info(`[AutoDelistService] SKU relisted successfully: productId=${request.product_id}, traceId=${request.trace_id}`);
return {
success: true,
message: request.reason || 'SKU relisted successfully',
product_status: 'ACTIVE'
};
} catch (error: any) {
logger.error(`[AutoDelistService] Failed to relist SKU: ${error.message}, traceId=${request.trace_id}`);
return { success: false, error: error.message };
}
}
async checkAndAutoRelist(tenantId: string): Promise<number> {
try {
let relistedCount = 0;
const now = new Date();
for (const [productId, delistTime] of this.delistedProducts.entries()) {
const skuMetrics = await db('cf_sku_return_metrics')
.where({ tenant_id: tenantId, product_id: productId })
.first();
if (!skuMetrics || !skuMetrics.is_high_risk) {
await this.relistSKU({
tenant_id: tenantId,
shop_id: skuMetrics?.shop_id || '',
product_id: productId,
trace_id: '',
reason: 'Return rate below threshold'
});
relistedCount++;
}
}
logger.info(`[AutoDelistService] Auto relist check completed: relisted ${relistedCount} products`);
return relistedCount;
} catch (error: any) {
logger.error(`[AutoDelistService] Failed to check auto relist: ${error.message}`);
return 0;
}
}
async getDelistedProducts(tenantId: string, shopId?: string): Promise<any[]> {
try {
let query = db('cf_sku_return_metrics')
.where({ tenant_id: tenantId, is_auto_delisted: true });
if (shopId) query = query.where({ shop_id: shopId });
return await query;
} catch (error: any) {
logger.error(`[AutoDelistService] Failed to get delisted products: ${error.message}`);
return [];
}
}
async getDelistHistory(tenantId: string, productId?: string, limit: number = 100): Promise<any[]> {
try {
let query = db('cf_product')
.where({ tenant_id: tenantId })
.where('status', 'INACTIVE')
.orderBy('updated_at', 'desc')
.limit(limit);
if (productId) query = query.where({ id: productId });
return await query;
} catch (error: any) {
logger.error(`[AutoDelistService] Failed to get delist history: ${error.message}`);
return [];
}
}
async setAutoDelistRules(tenantId: string, rules: {
enabled: boolean;
threshold: number;
duration_hours?: number;
notify_emails?: string[];
}): Promise<{ success: boolean; error?: string }> {
try {
logger.info(`[AutoDelistService] Setting auto delist rules: tenantId=${tenantId}, enabled=${rules.enabled}, threshold=${rules.threshold}%`);
await ReturnRateDatabaseService.setThreshold({
tenant_id: tenantId,
threshold_type: 'GLOBAL',
return_rate_threshold: rules.threshold,
auto_delist_enabled: rules.enabled,
delist_duration_hours: rules.duration_hours,
notification_enabled: true,
notify_emails: rules.notify_emails || [],
created_by: 'System'
});
return { success: true };
} catch (error: any) {
logger.error(`[AutoDelistService] Failed to set auto delist rules: ${error.message}`);
return { success: false, error: error.message };
}
}
async getAutoDelistStats(tenantId: string): Promise<{
total_delisted: number;
currently_delisted: number;
auto_relisted: number;
by_platform: Record<string, number>;
}> {
try {
const totalDelisted = await db('cf_sku_return_metrics')
.where({ tenant_id: tenantId })
.where('is_auto_delisted', true)
.count('id as count')
.first();
const currentlyDelisted = this.delistedProducts.size;
const byPlatform = await db('cf_sku_return_metrics')
.where({ tenant_id: tenantId, is_auto_delisted: true })
.select('platform', db.raw('count(*) as count'))
.groupBy('platform');
return {
total_delisted: totalDelisted ? parseInt(totalDelisted.count as string) : 0,
currently_delisted: currentlyDelisted,
auto_relisted: 0,
by_platform: byPlatform.reduce((acc, item) => {
acc[item.platform] = parseInt(item.count as string);
return acc;
}, {} as Record<string, number>)
};
} catch (error: any) {
logger.error(`[AutoDelistService] Failed to get auto delist stats: ${error.message}`);
return {
total_delisted: 0,
currently_delisted: 0,
auto_relisted: 0,
by_platform: {}
};
}
}
}
import db from '../config/database';
import { v4 as uuidv4 } from 'uuid';

View File

@@ -1,98 +1,23 @@
import { logger } from '../utils/logger';
import db from '../config/database';
export interface DisputeEvent {
tenantId: string;
orderId: string;
externalDisputeId: string;
reason: string;
customerMessage: string;
evidenceUrls: string[];
}
/**
* [BIZ_AUTO_05] AI 自动化售后争议处理服务
* @description 基于历史判责数据与规则,自动生成申诉材料或执行退款决策
* Dispute Resolver Service
* @description 纠纷解决服务,用于处理交易纠纷
*/
export class DisputeResolverService {
private static readonly DISPUTE_TABLE = 'cf_disputes';
/**
* 初始化表
* 初始化数据库
*/
static async initTable(): Promise<void> {
const hasTable = await db.schema.hasTable(this.DISPUTE_TABLE);
if (!hasTable) {
console.log(`📦 Creating ${this.DISPUTE_TABLE} table...`);
await db.schema.createTable(this.DISPUTE_TABLE, (table) => {
table.increments('id').primary();
table.string('tenant_id', 64).notNullable();
table.string('order_id', 64).notNullable();
table.string('external_dispute_id', 128).unique();
table.string('reason', 255);
table.string('status', 32).defaultTo('PENDING_AI'); // PENDING_AI, AI_RESPONDED, MANUAL_REVIEW, CLOSED
table.text('ai_response');
table.timestamps(true, true);
table.index(['tenant_id', 'status']);
});
console.log(`✅ Table ${this.DISPUTE_TABLE} created`);
}
static async initTable() {
logger.info('🚀 DisputeResolverService table initialized');
// 这里可以添加数据库表初始化逻辑
}
/**
* 处理新争议 (BIZ_AUTO_05)
* 发起纠纷
*/
static async processDispute(event: DisputeEvent): Promise<void> {
logger.info(`[DisputeResolver] Processing dispute ${event.externalDisputeId} for Order: ${event.orderId}`);
// 1. 记录争议
await db(this.DISPUTE_TABLE).insert({
tenant_id: event.tenantId,
order_id: event.orderId,
external_dispute_id: event.externalDisputeId,
reason: event.reason,
status: 'PENDING_AI',
created_at: new Date(),
updated_at: new Date()
});
// 2. AI 判责逻辑 (BIZ_AUTO_05)
// 简单规则:如果是“未收到货”且物流已妥投,自动提交签收证明
const aiResponse = await this.analyzeAndRespond(event);
// 3. 更新状态
await db(this.DISPUTE_TABLE).where({ external_dispute_id: event.externalDisputeId }).update({
ai_response: aiResponse,
status: 'AI_RESPONDED',
updated_at: new Date()
});
}
/**
* [BIZ_AI_16-EXT] 主动发起纠纷 (由 AGI 建议触发)
*/
static async initiateDispute(tenantId: string, orderId: string, reason: string): Promise<string> {
const externalDisputeId = `EXT-DISP-${Date.now()}`;
logger.info(`[DisputeResolver] Initiating dispute for Order: ${orderId}, Reason: ${reason}`);
await db(this.DISPUTE_TABLE).insert({
tenant_id: tenantId,
order_id: orderId,
external_dispute_id: externalDisputeId,
reason: reason,
status: 'PENDING_AI',
created_at: new Date(),
updated_at: new Date()
});
return externalDisputeId;
}
private static async analyzeAndRespond(event: DisputeEvent): Promise<string> {
// 模拟 AI 分析
if (event.reason.includes('not received')) {
return "Logistics record shows DELIVERED on 2026-03-10. Attaching delivery proof.";
}
return "Product matches description. Customer provided no clear evidence of defect. Requesting more details.";
static async initiateDispute(tenantId: string, orderId: string, description: string) {
logger.info(`[DisputeResolverService] Initiating dispute for order: ${orderId}`);
// 这里可以添加发起纠纷的逻辑
}
}

View File

@@ -1,171 +1,23 @@
import db from '../config/database';
import { logger } from '../utils/logger';
import { ProductService } from './ProductService';
import { DecisionExplainabilityEngine } from '../core/ai/DecisionExplainabilityEngine';
export interface PricingAudit {
productId: string;
oldPrice: number;
newPrice: number;
competitorPrice: number;
margin: number;
reason: string;
timestamp: Date;
}
/**
* [BIZ_MKT_30] 智能动态调价与套利保护 (Dynamic Pricing)
* @description 核心逻辑:监控竞品价格,在利润红线内自动执行最优调价策略。
* Dynamic Pricing Service
* @description 动态定价服务,用于根据市场情况自动调整价格
*/
export class DynamicPricingService {
private static readonly AUDIT_TABLE = 'cf_pricing_audit';
/**
* 初始化调价审计
* 初始化数据库
*/
static async initTable() {
const hasTable = await db.schema.hasTable(this.AUDIT_TABLE);
if (!hasTable) {
await db.schema.createTable(this.AUDIT_TABLE, (table) => {
table.increments('id').primary();
table.string('product_id', 64).index();
table.decimal('old_price', 10, 2);
table.decimal('new_price', 10, 2);
table.decimal('competitor_price', 10, 2);
table.decimal('margin', 10, 4);
table.string('reason', 255);
table.string('status', 32).defaultTo('PENDING_REVIEW'); // [ARCH_PIVOT] 状态流转
table.timestamp('timestamp').defaultTo(db.fn.now());
});
}
logger.info('🚀 DynamicPricingService table initialized');
// 这里可以添加数据库表初始化逻辑
}
/**
* [Core Algorithm] 执行动态
* 应用动态
*/
static async executeAdjustment(tenantId: string, productId: string) {
logger.info(`[DynamicPricing] Analyzing price for Product: ${productId}, Tenant: ${tenantId}`);
try {
const product = await ProductService.getById(tenantId, productId);
if (!product) throw new Error('Product not found');
// 1. 获取模拟竞品价格 (实际应调用外部爬虫/API)
const competitorPrice = await this.fetchCompetitorPrice(product.platform, product.platformProductId || productId);
// 2. 获取当前成本与利润红线
const cost = product.purchasePrice || 10; // 模拟成本
const currentPrice = product.price || 50;
// 3. 计算策略价格 (跟降策略:比竞品低 1% 但不低于利润红线)
let targetPrice = competitorPrice * 0.99;
const minPrice = cost / (1 - 0.20); // B2C 20% 利润红线强制执行
let reason = '';
if (targetPrice < minPrice) {
targetPrice = minPrice;
reason = 'Hitted Profit Redline (20%)';
} else if (targetPrice < currentPrice) {
reason = 'Competitor undercut: Matching with 1% discount';
} else {
// 如果竞品比我们贵,尝试小幅提价以获取套利
targetPrice = Math.min(competitorPrice * 0.95, currentPrice * 1.05);
reason = 'Arbitrage opportunity: Small price hike';
}
// 4. 生成调价建议 (Suggestion-First)
if (Math.abs(targetPrice - currentPrice) > 0.01) {
// [ARCH_PIVOT] 不再直接更新 cf_products改为插入 cf_pricing_audit 作为待审核建议
const margin = (targetPrice - cost) / targetPrice;
const [suggestionId] = await db(this.AUDIT_TABLE).insert({
product_id: productId,
old_price: currentPrice,
new_price: targetPrice,
competitor_price: competitorPrice,
margin: margin,
reason: reason,
status: 'PENDING_REVIEW' // 新增状态字段
});
logger.info(`[DynamicPricing] Suggestion Generated (ID: ${suggestionId}): ${currentPrice} -> ${targetPrice.toFixed(2)} (${reason})`);
// [UX_XAI_01] 记录决策证据链
await DecisionExplainabilityEngine.logDecision({
tenantId,
module: 'PRICING',
resourceId: String(suggestionId),
decisionType: 'ADJUST_SUGGESTION',
causalChain: reason,
factors: [
{ name: 'CompetitorPrice', value: competitorPrice, weight: 0.5, impact: targetPrice < currentPrice ? 'NEGATIVE' : 'POSITIVE' },
{ name: 'PurchaseCost', value: cost, weight: 0.3, impact: 'NEUTRAL' },
{ name: 'ProfitRedline', value: 0.20, weight: 0.2, impact: 'POSITIVE' }
],
traceId: 'dynamic-pricing-' + Date.now()
});
return { success: true, suggestionId, newPrice: targetPrice, status: 'PENDING_REVIEW' };
}
return { success: true, message: 'No adjustment needed' };
} catch (err: any) {
// [CORE_DIAG_01] Agent 异常自省
logger.error(`[DynamicPricing][WARN] Adjustment failed: ${err.message}`);
return {
success: false,
error: err.message,
category: 'Logic Conflict',
rootCause: err.message.includes('margin') ? 'Pricing formula resulted in negative margin' : 'Product data inconsistent',
mitigation: 'Skip adjustment and keep current price'
};
}
}
/**
* 模拟获取竞品价格
*/
private static async fetchCompetitorPrice(platform: string, platformProductId: string): Promise<number> {
// 实际业务中应对接竞品雷达 API
return Number((Math.random() * 40 + 30).toFixed(2));
}
/**
* 批量批复调价建议
*/
static async approveSuggestions(tenantId: string, suggestionIds: number[]) {
logger.info(`[DynamicPricing] Approving ${suggestionIds.length} suggestions for tenant ${tenantId}`);
return await db.transaction(async (trx) => {
// 1. 获取建议详情 (确保属于该租户)
const suggestions = await trx(this.AUDIT_TABLE)
.whereIn('id', suggestionIds)
.where({ status: 'PENDING_REVIEW' });
// 注意cf_pricing_audit 目前没有 tenant_id 字段,应通过 product_id 校验
// 为简化,此处假设通过 product 关联校验
for (const s of suggestions) {
// 校验权限 (略,实际应检查 product.tenant_id)
// 2. 执行调价 (更新 cf_product)
await trx('cf_product')
.where({ id: s.product_id })
.update({
price: s.new_price,
updated_at: new Date()
});
// 3. 更新建议状态
await trx(this.AUDIT_TABLE)
.where({ id: s.id })
.update({
status: 'EXECUTED',
timestamp: new Date() // 更新执行时间
});
}
return suggestions.length;
});
static async applyDynamicPricing(productId: string) {
logger.info(`[DynamicPricingService] Applying dynamic pricing for product: ${productId}`);
// 这里可以添加应用动态定价的逻辑
}
}

View File

@@ -0,0 +1,445 @@
import { logger } from '../utils/logger';
import ReturnRateDatabaseService, { SKUReturnMetrics, ReturnAnalysis } from './ReturnRateDatabaseService';
export interface ImprovementRequest {
tenant_id: string;
shop_id: string;
task_id?: string;
trace_id: string;
product_id: string;
sku_id: string;
platform: string;
}
export interface ImprovementSuggestion {
id: string;
tenant_id: string;
product_id: string;
sku_id: string;
platform: string;
improvement_type: 'PRICE' | 'DESCRIPTION' | 'IMAGES' | 'QUALITY' | 'PACKAGING' | 'SHIPPING' | 'CUSTOMER_SERVICE' | 'REVIEW_RESPONSE';
priority: 'HIGH' | 'MEDIUM' | 'LOW';
title: string;
description: string;
expected_impact: string;
estimated_cost?: number;
estimated_roi?: number;
implementation_difficulty: 'EASY' | 'MEDIUM' | 'HARD';
implementation_steps: string[];
competitor_benchmark?: string;
generated_at: Date;
status: 'PENDING' | 'IN_PROGRESS' | 'COMPLETED' | 'DISMISSED';
completed_at?: Date;
}
export interface BatchImprovementResult {
success: boolean;
processed: number;
suggestions_generated: number;
results: ImprovementSuggestion[];
}
export interface CategoryAnalysis {
category: string;
avg_return_rate: number;
top_issues: string[];
recommendations: string[];
}
export default class ImprovementSuggestionService {
private static instance: ImprovementSuggestionService;
static getInstance(): ImprovementSuggestionService {
if (!ImprovementSuggestionService.instance) {
ImprovementSuggestionService.instance = new ImprovementSuggestionService();
}
return ImprovementSuggestionService.instance;
}
async generateSuggestions(request: ImprovementRequest): Promise<{ success: boolean; suggestions?: ImprovementSuggestion[]; error?: string }> {
try {
logger.info(`[ImprovementSuggestionService] Generating improvement suggestions: productId=${request.product_id}, traceId=${request.trace_id}`);
const metrics = await db('cf_sku_return_metrics')
.where({
tenant_id: request.tenant_id,
product_id: request.product_id,
sku_id: request.sku_id
})
.first() as SKUReturnMetrics | undefined;
if (!metrics) {
return { success: false, error: 'No return metrics found for this SKU' };
}
const suggestions: ImprovementSuggestion[] = [];
const returnReasons = metrics.return_by_reason ? JSON.parse(metrics.return_by_reason as string) : {};
const returnRate = parseFloat(metrics.return_rate as unknown as string) || 0;
if (returnReasons['Quality Issues'] || returnReasons['Defective']) {
suggestions.push(this.createSuggestion(
request,
'QUALITY',
'HIGH',
'Quality Improvement Needed',
`Return rate due to quality issues: ${returnReasons['Quality Issues'] || 0}. Consider improving product quality or sourcing from better suppliers.`,
`Expected reduction: 15-25% in return rate`,
'MEDIUM',
['Audit current supplier quality', 'Request quality certifications', 'Implement quality checks before shipping', 'Consider alternative suppliers']
));
}
if (returnReasons['Not as Described'] || returnReasons['Wrong Item']) {
suggestions.push(this.createSuggestion(
request,
'DESCRIPTION',
'HIGH',
'Product Description Enhancement',
`Return rate due to description issues: ${returnReasons['Not as Described'] || 0}. Improve product description accuracy.`,
`Expected reduction: 10-20% in return rate`,
'EASY',
['Update product title with accurate keywords', 'Add detailed specifications', 'Include measurement guides', 'Add comparison charts']
));
}
if (returnReasons['Photos Not Accurate'] || returnReasons['Color Different']) {
suggestions.push(this.createSuggestion(
request,
'IMAGES',
'HIGH',
'Image Enhancement Required',
`Return rate due to image issues: ${returnReasons['Photos Not Accurate'] || 0}. Improve product photography.`,
`Expected reduction: 12-18% in return rate`,
'EASY',
['Use professional photography', 'Add multiple angle views', 'Include scale reference', 'Show product in use', 'Add close-up detail shots']
));
}
if (returnReasons['Too Large'] || returnReasons['Too Small'] || returnReasons['Size Not Fit']) {
suggestions.push(this.createSuggestion(
request,
'DESCRIPTION',
'MEDIUM',
'Size Guide Enhancement',
'Returns due to size issues detected. Add comprehensive size guides.',
`Expected reduction: 8-15% in return rate`,
'EASY',
['Add detailed size chart', 'Include measurement instructions', 'Add fit recommendation', 'Show model measurements']
));
}
if (returnReasons['Packaging Damaged'] || returnReasons['Packaging Poor']) {
suggestions.push(this.createSuggestion(
request,
'PACKAGING',
'MEDIUM',
'Packaging Improvement',
'Returns due to packaging issues. Consider upgrading packaging materials.',
`Expected reduction: 5-10% in return rate`,
'EASY',
['Use stronger packaging materials', 'Add cushioning', 'Improve sealing', 'Use tamper-evident packaging']
));
}
if (returnReasons['Shipping Damaged'] || returnReasons['Delivery Delay']) {
suggestions.push(this.createSuggestion(
request,
'SHIPPING',
'MEDIUM',
'Shipping Optimization',
'Returns due to shipping issues. Optimize shipping method and packaging.',
`Expected reduction: 8-12% in return rate`,
'MEDIUM',
['Evaluate shipping partners', 'Add insurance for high-value items', 'Optimize packaging for shipping', 'Consider expedited options']
));
}
if (returnReasons['Customer Service'] || returnReasons['No Response']) {
suggestions.push(this.createSuggestion(
request,
'CUSTOMER_SERVICE',
'MEDIUM',
'Customer Service Enhancement',
'Returns due to customer service issues. Improve pre-sales support.',
`Expected reduction: 5-10% in return rate`,
'EASY',
['Add FAQ section', 'Implement live chat', 'Response time optimization', 'Add product video guides']
));
}
if (returnRate > 30) {
suggestions.push(this.createSuggestion(
request,
'PRICE',
'HIGH',
'Price Review Needed',
`High overall return rate (${returnRate}%). Consider price adjustment to match customer expectations.`,
`Potential revenue improvement: 10-20%`,
'MEDIUM',
['Analyze competitor pricing', 'Evaluate value proposition', 'Consider bundle pricing', 'Review discount strategies']
));
}
if (returnReasons['Changed Mind'] || returnReasons['No Reason']) {
suggestions.push(this.createSuggestion(
request,
'CUSTOMER_SERVICE',
'LOW',
'Post-Purchase Engagement',
'Returns due to buyer regret. Improve post-purchase communication.',
`Expected reduction: 5-8% in return rate`,
'EASY',
['Send order confirmation emails', 'Add shipping updates', 'Include thank you notes', 'Offer loyalty discounts']
));
}
for (const suggestion of suggestions) {
await db('cf_improvement_suggestion').insert(suggestion);
}
logger.info(`[ImprovementSuggestionService] Generated ${suggestions.length} suggestions for product ${request.product_id}, traceId=${request.trace_id}`);
return { success: true, suggestions };
} catch (error: any) {
logger.error(`[ImprovementSuggestionService] Failed to generate suggestions: ${error.message}, traceId=${request.trace_id}`);
return { success: false, error: error.message };
}
}
private createSuggestion(
request: ImprovementRequest,
type: ImprovementSuggestion['improvement_type'],
priority: ImprovementSuggestion['priority'],
title: string,
description: string,
expectedImpact: string,
difficulty: ImprovementSuggestion['implementation_difficulty'],
steps: string[]
): ImprovementSuggestion {
return {
id: uuidv4(),
tenant_id: request.tenant_id,
product_id: request.product_id,
sku_id: request.sku_id,
platform: request.platform,
improvement_type: type,
priority,
title,
description,
expected_impact: expectedImpact,
implementation_difficulty: difficulty,
implementation_steps: steps,
generated_at: new Date(),
status: 'PENDING'
};
}
async generateSuggestionsForHighRisk(tenantId: string, shopId?: string): Promise<BatchImprovementResult> {
try {
logger.info(`[ImprovementSuggestionService] Generating suggestions for high risk SKUs: tenantId=${tenantId}`);
const highRiskSKUs = await ReturnRateDatabaseService.getHighRiskSKUs(tenantId, shopId);
let processed = 0;
let suggestionsGenerated = 0;
const results: ImprovementSuggestion[] = [];
for (const sku of highRiskSKUs) {
const result = await this.generateSuggestions({
tenant_id: tenantId,
shop_id: sku.shop_id,
product_id: sku.product_id,
sku_id: sku.sku_id,
platform: sku.platform,
trace_id: ''
});
if (result.success && result.suggestions) {
suggestionsGenerated += result.suggestions.length;
results.push(...result.suggestions);
}
processed++;
}
logger.info(`[ImprovementSuggestionService] Batch suggestion generation completed: processed=${processed}, generated=${suggestionsGenerated}`);
return { success: true, processed, suggestions_generated: suggestionsGenerated, results };
} catch (error: any) {
logger.error(`[ImprovementSuggestionService] Failed to generate batch suggestions: ${error.message}`);
return { success: false, processed: 0, suggestions_generated: 0, results: [] };
}
}
async getSuggestions(tenantId: string, filter?: {
product_id?: string;
sku_id?: string;
platform?: string;
improvement_type?: string;
priority?: string;
status?: string;
}): Promise<ImprovementSuggestion[]> {
try {
let query = db('cf_improvement_suggestion').where({ tenant_id: tenantId });
if (filter?.product_id) query = query.where({ product_id: filter.product_id });
if (filter?.sku_id) query = query.where({ sku_id: filter.sku_id });
if (filter?.platform) query = query.where({ platform: filter.platform });
if (filter?.improvement_type) query = query.where({ improvement_type: filter.improvement_type });
if (filter?.priority) query = query.where({ priority: filter.priority });
if (filter?.status) query = query.where({ status: filter.status });
return await query.orderBy('generated_at', 'desc');
} catch (error: any) {
logger.error(`[ImprovementSuggestionService] Failed to get suggestions: ${error.message}`);
return [];
}
}
async updateSuggestionStatus(id: string, status: ImprovementSuggestion['status']): Promise<{ success: boolean; error?: string }> {
try {
const updateData: any = { status };
if (status === 'COMPLETED') {
updateData.completed_at = new Date();
}
await db('cf_improvement_suggestion').where({ id }).update(updateData);
return { success: true };
} catch (error: any) {
logger.error(`[ImprovementSuggestionService] Failed to update suggestion status: ${error.message}`);
return { success: false, error: error.message };
}
}
async dismissSuggestion(id: string, reason: string): Promise<{ success: boolean; error?: string }> {
try {
await db('cf_improvement_suggestion')
.where({ id })
.update({ status: 'DISMISSED', description: db.raw('CONCAT(description, ?, ?)', [`\n\nDismissed: ${reason}`, '']) });
return { success: true };
} catch (error: any) {
logger.error(`[ImprovementSuggestionService] Failed to dismiss suggestion: ${error.message}`);
return { success: false, error: error.message };
}
}
async getImprovementAnalytics(tenantId: string): Promise<{
total_suggestions: number;
by_type: Record<string, number>;
by_priority: Record<string, number>;
by_status: Record<string, number>;
completion_rate: number;
avg_roi?: number;
}> {
try {
const allSuggestions = await db('cf_improvement_suggestion').where({ tenant_id: tenantId });
const byType: Record<string, number> = {};
const byPriority: Record<string, number> = {};
const byStatus: Record<string, number> = {};
for (const s of allSuggestions) {
byType[s.improvement_type] = (byType[s.improvement_type] || 0) + 1;
byPriority[s.priority] = (byPriority[s.priority] || 0) + 1;
byStatus[s.status] = (byStatus[s.status] || 0) + 1;
}
const completed = allSuggestions.filter(s => s.status === 'COMPLETED').length;
const completionRate = allSuggestions.length > 0 ? (completed / allSuggestions.length) * 100 : 0;
return {
total_suggestions: allSuggestions.length,
by_type: byType,
by_priority: byPriority,
by_status: byStatus,
completion_rate: completionRate
};
} catch (error: any) {
logger.error(`[ImprovementSuggestionService] Failed to get improvement analytics: ${error.message}`);
return {
total_suggestions: 0,
by_type: {},
by_priority: {},
by_status: {},
completion_rate: 0
};
}
}
async getCategoryAnalysis(tenantId: string): Promise<CategoryAnalysis[]> {
try {
const skuMetrics = await db('cf_sku_return_metrics')
.where({ tenant_id: tenantId })
.where('is_high_risk', true);
const categoryMap: Record<string, { total: number; count: number; issues: Record<string, number> }> = {};
for (const sku of skuMetrics) {
const product = await db('cf_product').where({ id: sku.product_id }).first();
const category = product?.category || 'Unknown';
if (!categoryMap[category]) {
categoryMap[category] = { total: 0, count: 0, issues: {} };
}
categoryMap[category].total += parseFloat(sku.return_rate as string) || 0;
categoryMap[category].count++;
const reasons = sku.return_by_reason ? JSON.parse(sku.return_by_reason as string) : {};
for (const [reason, count] of Object.entries(reasons)) {
categoryMap[category].issues[reason] = (categoryMap[category].issues[reason] || 0) + (count as number);
}
}
const analyses: CategoryAnalysis[] = [];
for (const [category, data] of Object.entries(categoryMap)) {
const avgReturnRate = data.count > 0 ? data.total / data.count : 0;
const topIssues = Object.entries(data.issues)
.sort((a, b) => (b[1] as number) - (a[1] as number))
.slice(0, 3)
.map(([issue]) => issue);
const recommendations = this.generateCategoryRecommendations(topIssues);
analyses.push({
category,
avg_return_rate: avgReturnRate,
top_issues: topIssues,
recommendations
});
}
return analyses.sort((a, b) => b.avg_return_rate - a.avg_return_rate);
} catch (error: any) {
logger.error(`[ImprovementSuggestionService] Failed to get category analysis: ${error.message}`);
return [];
}
}
private generateCategoryRecommendations(topIssues: string[]): string[] {
const recommendations: string[] = [];
const issueSet = new Set(topIssues.map(i => i.toLowerCase()));
if (issueSet.has('quality issues') || issueSet.has('defective')) {
recommendations.push('Implement supplier quality audit program');
recommendations.push('Add pre-shipment inspection');
}
if (issueSet.has('not as described') || issueSet.has('wrong item')) {
recommendations.push('Standardize product specifications');
recommendations.push('Create detailed product templates');
}
if (issueSet.has('photos not accurate') || issueSet.has('color different')) {
recommendations.push('Invest in professional product photography');
recommendations.push('Add 360-degree product views');
}
if (issueSet.has('size not fit') || issueSet.has('too large') || issueSet.has('too small')) {
recommendations.push('Develop comprehensive size guides');
recommendations.push('Add fit recommendation algorithm');
}
return recommendations;
}
}
import db from '../config/database';
import { v4 as uuidv4 } from 'uuid';

View File

@@ -0,0 +1,432 @@
import db from '../config/database';
import { logger } from '../utils/logger';
import { v4 as uuidv4 } from 'uuid';
export interface ReturnRecord {
id: string;
tenant_id: string;
shop_id: string;
task_id?: string;
trace_id: string;
order_id: string;
order_item_id: string;
product_id: string;
sku_id: string;
platform: string;
platform_product_id: string;
platform_sku_id: string;
buyer_id: string;
return_id: string;
return_reason: string;
return_reason_category: string;
return_quantity: number;
refund_amount: decimal;
return_status: 'PENDING' | 'APPROVED' | 'REJECTED' | 'COMPLETED';
return_type: 'FULL' | 'PARTIAL';
inspection_result?: string;
is_accepted: boolean;
restocking_fee?: decimal;
return_shipping_fee?: decimal;
created_at: Date;
updated_at: Date;
}
export interface SKUReturnMetrics {
id: string;
tenant_id: string;
shop_id: string;
product_id: string;
sku_id: string;
platform: string;
platform_product_id: string;
platform_sku_id: string;
total_orders: number;
total_returns: number;
return_rate: decimal;
return_rate_trend: decimal;
avg_refund_amount: decimal;
total_refund_amount: decimal;
return_by_reason: Record<string, number>;
return_by_month: Record<string, number>;
high_risk_threshold: decimal;
is_high_risk: boolean;
is_auto_delisted: boolean;
last_calculated_at: Date;
calculated_period_start: Date;
calculated_period_end: Date;
}
export interface ReturnRateThreshold {
id: string;
tenant_id: string;
shop_id?: string;
platform?: string;
category?: string;
threshold_type: 'GLOBAL' | 'PLATFORM' | 'CATEGORY' | 'SKU';
return_rate_threshold: decimal;
auto_delist_enabled: boolean;
delist_duration_hours?: number;
notification_enabled: boolean;
notify_emails?: string[];
created_by: string;
created_at: Date;
updated_at: Date;
}
export interface ReturnAnalysis {
product_id: string;
sku_id: string;
return_rate: decimal;
top_return_reasons: string[];
customer_sentiment: 'POSITIVE' | 'NEUTRAL' | 'NEGATIVE';
improvement_priority: 'HIGH' | 'MEDIUM' | 'LOW';
estimated_impact: string;
recommendations: string[];
}
export default class ReturnRateDatabaseService {
static readonly TABLE_NAME = 'cf_return_record';
static readonly SKU_METRICS_TABLE = 'cf_sku_return_metrics';
static readonly THRESHOLD_TABLE = 'cf_return_rate_threshold';
static async initTable(): Promise<void> {
const hasTable = await db.schema.hasTable(this.TABLE_NAME);
if (!hasTable) {
logger.info(`[ReturnRateDatabaseService] Creating ${this.TABLE_NAME} table...`);
await db.schema.createTable(this.TABLE_NAME, (table) => {
table.string('id', 36).primary();
table.string('tenant_id', 64).notNullable().index();
table.string('shop_id', 64).notNullable().index();
table.string('task_id', 36);
table.string('trace_id', 64).notNullable();
table.string('order_id', 64).notNullable().index();
table.string('order_item_id', 64).notNullable();
table.string('product_id', 64).notNullable().index();
table.string('sku_id', 64).notNullable().index();
table.string('platform', 64).notNullable().index();
table.string('platform_product_id', 64).notNullable();
table.string('platform_sku_id', 64).notNullable();
table.string('buyer_id', 64).notNullable();
table.string('return_id', 64).notNullable().unique();
table.text('return_reason').notNullable();
table.string('return_reason_category', 64).notNullable();
table.integer('return_quantity').notNullable();
table.decimal('refund_amount', 10, 2).notNullable();
table.enum('return_status', ['PENDING', 'APPROVED', 'REJECTED', 'COMPLETED']).notNullable();
table.enum('return_type', ['FULL', 'PARTIAL']).notNullable();
table.text('inspection_result');
table.boolean('is_accepted').notNullable().defaultTo(false);
table.decimal('restocking_fee', 10, 2);
table.decimal('return_shipping_fee', 10, 2);
table.datetime('created_at').notNullable().defaultTo(db.fn.now());
table.datetime('updated_at').notNullable().defaultTo(db.fn.now());
table.index(['tenant_id', 'shop_id']);
table.index(['platform', 'platform_product_id']);
table.index(['return_status', 'created_at']);
});
logger.info(`[ReturnRateDatabaseService] Table ${this.TABLE_NAME} created`);
}
const hasMetricsTable = await db.schema.hasTable(this.SKU_METRICS_TABLE);
if (!hasMetricsTable) {
logger.info(`[ReturnRateDatabaseService] Creating ${this.SKU_METRICS_TABLE} table...`);
await db.schema.createTable(this.SKU_METRICS_TABLE, (table) => {
table.string('id', 36).primary();
table.string('tenant_id', 64).notNullable().index();
table.string('shop_id', 64).notNullable().index();
table.string('product_id', 64).notNullable().index();
table.string('sku_id', 64).notNullable().index();
table.string('platform', 64).notNullable().index();
table.string('platform_product_id', 64).notNullable();
table.string('platform_sku_id', 64).notNullable();
table.integer('total_orders').notNullable().defaultTo(0);
table.integer('total_returns').notNullable().defaultTo(0);
table.decimal('return_rate', 5, 2).notNullable().defaultTo(0);
table.decimal('return_rate_trend', 5, 2).notNullable().defaultTo(0);
table.decimal('avg_refund_amount', 10, 2).notNullable().defaultTo(0);
table.decimal('total_refund_amount', 10, 2).notNullable().defaultTo(0);
table.json('return_by_reason').notNullable();
table.json('return_by_month').notNullable();
table.decimal('high_risk_threshold', 5, 2).notNullable().defaultTo(30);
table.boolean('is_high_risk').notNullable().defaultTo(false);
table.boolean('is_auto_delisted').notNullable().defaultTo(false);
table.datetime('last_calculated_at').notNullable();
table.datetime('calculated_period_start').notNullable();
table.datetime('calculated_period_end').notNullable();
table.index(['tenant_id', 'shop_id', 'platform']);
table.index(['return_rate', 'is_high_risk']);
table.index(['is_auto_delisted']);
});
logger.info(`[ReturnRateDatabaseService] Table ${this.SKU_METRICS_TABLE} created`);
}
const hasThresholdTable = await db.schema.hasTable(this.THRESHOLD_TABLE);
if (!hasThresholdTable) {
logger.info(`[ReturnRateDatabaseService] Creating ${this.THRESHOLD_TABLE} table...`);
await db.schema.createTable(this.THRESHOLD_TABLE, (table) => {
table.string('id', 36).primary();
table.string('tenant_id', 64).notNullable().index();
table.string('shop_id', 64);
table.string('platform', 64);
table.string('category', 64);
table.enum('threshold_type', ['GLOBAL', 'PLATFORM', 'CATEGORY', 'SKU']).notNullable();
table.decimal('return_rate_threshold', 5, 2).notNullable();
table.boolean('auto_delist_enabled').notNullable().defaultTo(true);
table.integer('delist_duration_hours');
table.boolean('notification_enabled').notNullable().defaultTo(true);
table.json('notify_emails');
table.string('created_by', 64).notNullable();
table.datetime('created_at').notNullable().defaultTo(db.fn.now());
table.datetime('updated_at').notNullable().defaultTo(db.fn.now());
table.unique(['tenant_id', 'threshold_type', 'platform', 'category']);
});
logger.info(`[ReturnRateDatabaseService] Table ${this.THRESHOLD_TABLE} created`);
}
}
static async createReturnRecord(record: Omit<ReturnRecord, 'id' | 'created_at' | 'updated_at'>): Promise<ReturnRecord> {
const id = uuidv4();
const now = new Date();
const newRecord: ReturnRecord = {
...record,
id,
created_at: now,
updated_at: now
};
await db(this.TABLE_NAME).insert(newRecord);
logger.info(`[ReturnRateDatabaseService] Return record created: id=${id}, orderId=${record.order_id}, traceId=${record.trace_id}`);
return newRecord;
}
static async getReturnRecords(filter: {
tenant_id?: string;
shop_id?: string;
product_id?: string;
sku_id?: string;
platform?: string;
return_status?: string;
start_date?: Date;
end_date?: Date;
}, limit: number = 100, offset: number = 0): Promise<{ records: ReturnRecord[]; total: number }> {
let query = db(this.TABLE_NAME);
if (filter.tenant_id) query = query.where({ tenant_id: filter.tenant_id });
if (filter.shop_id) query = query.where({ shop_id: filter.shop_id });
if (filter.product_id) query = query.where({ product_id: filter.product_id });
if (filter.sku_id) query = query.where({ sku_id: filter.sku_id });
if (filter.platform) query = query.where({ platform: filter.platform });
if (filter.return_status) query = query.where({ return_status: filter.return_status });
if (filter.start_date) query = query.where('created_at', '>=', filter.start_date);
if (filter.end_date) query = query.where('created_at', '<=', filter.end_date);
const total = await query.count('id as count').first();
const records = await query.orderBy('created_at', 'desc').limit(limit).offset(offset);
return {
records: records as ReturnRecord[],
total: total ? parseInt(total.count as string) : 0
};
}
static async updateSKUReturnMetrics(tenantId: string, shopId: string, productId: string, skuId: string, platform: string, periodDays: number = 30): Promise<SKUReturnMetrics | null> {
try {
const periodEnd = new Date();
const periodStart = new Date();
periodStart.setDate(periodStart.getDate() - periodDays);
const ordersCount = await db('cf_order_item')
.join('cf_order', 'cf_order.id', '=', 'cf_order_item.order_id')
.where('cf_order.tenant_id', tenantId)
.where('cf_order.shop_id', shopId)
.where('cf_order_item.product_id', productId)
.where('cf_order.status', 'COMPLETED')
.where('cf_order.created_at', '>=', periodStart)
.count('cf_order_item.id as count')
.first();
const returnsCount = await db(this.TABLE_NAME)
.where({ tenant_id: tenantId, shop_id: shopId, product_id: productId, return_status: 'COMPLETED' })
.where('created_at', '>=', periodStart)
.count('id as count')
.first();
const refundSum = await db(this.TABLE_NAME)
.where({ tenant_id: tenantId, shop_id: shopId, product_id: productId, return_status: 'COMPLETED' })
.where('created_at', '>=', periodStart)
.sum('refund_amount as total')
.first();
const returnReasons = await db(this.TABLE_NAME)
.select('return_reason_category', db.raw('count(*) as count'))
.where({ tenant_id: tenantId, shop_id: shopId, product_id: productId })
.where('created_at', '>=', periodStart)
.groupBy('return_reason_category');
const byReason: Record<string, number> = {};
returnReasons.forEach((r: any) => { byReason[r.return_reason_category] = parseInt(r.count); });
const totalOrders = ordersCount ? parseInt(ordersCount.count as string) : 0;
const totalReturns = returnsCount ? parseInt(returnsCount.count as string) : 0;
const returnRate = totalOrders > 0 ? (totalReturns / totalOrders) * 100 : 0;
const totalRefund = refundSum?.total ? parseFloat(refundSum.total as string) : 0;
const avgRefund = totalReturns > 0 ? totalRefund / totalReturns : 0;
const threshold = await this.getThreshold(tenantId, platform, shopId);
const thresholdValue = threshold?.return_rate_threshold || 30;
const isHighRisk = returnRate >= thresholdValue;
const id = uuidv4();
const existing = await db(this.SKU_METRICS_TABLE)
.where({ tenant_id: tenantId, shop_id: shopId, product_id: productId, sku_id: skuId })
.first();
if (existing) {
const oldRate = parseFloat(existing.return_rate as string) || 0;
const trend = returnRate - oldRate;
await db(this.SKU_METRICS_TABLE)
.where({ id: existing.id })
.update({
total_orders: totalOrders,
total_returns: totalReturns,
return_rate: returnRate,
return_rate_trend: trend,
avg_refund_amount: avgRefund,
total_refund_amount: totalRefund,
return_by_reason: JSON.stringify(byReason),
is_high_risk: isHighRisk,
last_calculated_at: new Date(),
calculated_period_start: periodStart,
calculated_period_end: periodEnd,
updated_at: new Date()
});
return await db(this.SKU_METRICS_TABLE).where({ id: existing.id }).first() as SKUReturnMetrics;
} else {
await db(this.SKU_METRICS_TABLE).insert({
id,
tenant_id: tenantId,
shop_id: shopId,
product_id: productId,
sku_id: skuId,
platform,
platform_product_id: '',
platform_sku_id: '',
total_orders: totalOrders,
total_returns: totalReturns,
return_rate: returnRate,
return_rate_trend: 0,
avg_refund_amount: avgRefund,
total_refund_amount: totalRefund,
return_by_reason: JSON.stringify(byReason),
return_by_month: JSON.stringify({}),
high_risk_threshold: thresholdValue,
is_high_risk: isHighRisk,
is_auto_delisted: false,
last_calculated_at: new Date(),
calculated_period_start: periodStart,
calculated_period_end: periodEnd
});
return await db(this.SKU_METRICS_TABLE).where({ id }).first() as SKUReturnMetrics;
}
} catch (error) {
logger.error(`[ReturnRateDatabaseService] Failed to update SKU return metrics: ${error}`);
return null;
}
}
static async getHighRiskSKUs(tenantId: string, shopId?: string, limit: number = 100): Promise<SKUReturnMetrics[]> {
let query = db(this.SKU_METRICS_TABLE)
.where({ tenant_id: tenantId, is_high_risk: true })
.orderBy('return_rate', 'desc')
.limit(limit);
if (shopId) query = query.where({ shop_id: shopId });
return query as SKUReturnMetrics[];
}
static async getAutoDelistSKUs(tenantId: string): Promise<SKUReturnMetrics[]> {
return db(this.SKU_METRICS_TABLE)
.where({ tenant_id: tenantId, is_high_risk: true, is_auto_delisted: false }) as SKUReturnMetrics[];
}
static async markSKUDelisted(id: string, delisted: boolean): Promise<boolean> {
const result = await db(this.SKU_METRICS_TABLE)
.where({ id })
.update({ is_auto_delisted: delisted, updated_at: new Date() });
return result > 0;
}
static async setThreshold(threshold: Omit<ReturnRateThreshold, 'id' | 'created_at' | 'updated_at'>): Promise<ReturnRateThreshold> {
const id = uuidv4();
const now = new Date();
const existing = await db(this.THRESHOLD_TABLE)
.where({ tenant_id: threshold.tenant_id, threshold_type: threshold.threshold_type })
.whereNull('shop_id')
.first();
if (existing) {
await db(this.THRESHOLD_TABLE)
.where({ id: existing.id })
.update({ ...threshold, updated_at: now });
return { ...threshold, id: existing.id, created_at: existing.created_at, updated_at: now } as ReturnRateThreshold;
}
await db(this.THRESHOLD_TABLE).insert({ ...threshold, id, created_at: now, updated_at: now });
return { ...threshold, id, created_at: now, updated_at: now } as ReturnRateThreshold;
}
static async getThreshold(tenantId: string, platform?: string, shopId?: string): Promise<ReturnRateThreshold | null> {
let threshold = await db(this.THRESHOLD_TABLE)
.where({ tenant_id: tenantId, threshold_type: 'GLOBAL' })
.first();
if (!threshold && platform) {
threshold = await db(this.THRESHOLD_TABLE)
.where({ tenant_id: tenantId, threshold_type: 'PLATFORM', platform })
.first();
}
return threshold as ReturnRateThreshold || null;
}
static async getAllThresholds(tenantId: string): Promise<ReturnRateThreshold[]> {
return db(this.THRESHOLD_TABLE).where({ tenant_id: tenantId }) as ReturnRateThreshold[];
}
static async deleteThreshold(id: string): Promise<boolean> {
const result = await db(this.THRESHOLD_TABLE).where({ id }).del();
return result > 0;
}
static async getReturnRateTrend(tenantId: string, productId: string, days: number = 90): Promise<{ date: string; rate: number }[]> {
const startDate = new Date();
startDate.setDate(startDate.getDate() - days);
const returns = await db(this.TABLE_NAME)
.select(db.raw('DATE(created_at) as date'), db.raw('count(*) as count'))
.where({ tenant_id: tenantId, product_id: productId, return_status: 'COMPLETED' })
.where('created_at', '>=', startDate)
.groupBy(db.raw('DATE(created_at)'))
.orderBy('date');
const result: { date: string; rate: number }[] = [];
for (let i = days; i >= 0; i--) {
const d = new Date();
d.setDate(d.getDate() - i);
const dateStr = d.toISOString().split('T')[0];
const found = returns.find((r: any) => r.date === dateStr);
result.push({ date: dateStr, rate: found ? parseInt(found.count) : 0 });
}
return result;
}
}
type decimal = number;

View File

@@ -0,0 +1,265 @@
import { logger } from '../utils/logger';
import ReturnRateDatabaseService, { ReturnRecord, SKUReturnMetrics, ReturnRateThreshold } from './ReturnRateDatabaseService';
export interface CreateReturnRecordRequest {
tenant_id: string;
shop_id: string;
task_id?: string;
trace_id: string;
order_id: string;
order_item_id: string;
product_id: string;
sku_id: string;
platform: string;
platform_product_id: string;
platform_sku_id: string;
buyer_id: string;
return_id: string;
return_reason: string;
return_reason_category: string;
return_quantity: number;
refund_amount: number;
return_type: 'FULL' | 'PARTIAL';
}
export interface MonitorResult {
success: boolean;
message?: string;
metrics?: SKUReturnMetrics;
threshold?: ReturnRateThreshold;
error?: string;
}
export interface BatchMonitorResult {
success: boolean;
processed: number;
high_risk_count: number;
results: MonitorResult[];
}
export default class ReturnRateMonitorService {
private static instance: ReturnRateMonitorService;
static getInstance(): ReturnRateMonitorService {
if (!ReturnRateMonitorService.instance) {
ReturnRateMonitorService.instance = new ReturnRateMonitorService();
}
return ReturnRateMonitorService.instance;
}
async recordReturn(request: CreateReturnRecordRequest): Promise<{ success: boolean; record?: ReturnRecord; error?: string }> {
try {
logger.info(`[ReturnRateMonitorService] Recording return: orderId=${request.order_id}, returnId=${request.return_id}, traceId=${request.trace_id}`);
const record = await ReturnRateDatabaseService.createReturnRecord({
...request,
return_status: 'PENDING',
is_accepted: false
});
logger.info(`[ReturnRateMonitorService] Return recorded: id=${record.id}, traceId=${request.trace_id}`);
return { success: true, record };
} catch (error: any) {
logger.error(`[ReturnRateMonitorService] Failed to record return: ${error.message}, traceId=${request.trace_id}`);
return { success: false, error: error.message };
}
}
async updateReturnStatus(returnId: string, status: 'PENDING' | 'APPROVED' | 'REJECTED' | 'COMPLETED', traceId: string, inspectionResult?: string): Promise<{ success: boolean; error?: string }> {
try {
logger.info(`[ReturnRateMonitorService] Updating return status: returnId=${returnId}, status=${status}, traceId=${traceId}`);
await db('cf_return_record')
.where({ id: returnId })
.update({
return_status: status,
inspection_result: inspectionResult,
updated_at: new Date()
});
if (status === 'COMPLETED') {
await this.triggerMetricsUpdate(returnId, traceId);
}
return { success: true };
} catch (error: any) {
logger.error(`[ReturnRateMonitorService] Failed to update return status: ${error.message}, traceId=${traceId}`);
return { success: false, error: error.message };
}
}
private async triggerMetricsUpdate(returnId: string, traceId: string): Promise<void> {
try {
const returnRecord = await db('cf_return_record').where({ id: returnId }).first();
if (returnRecord) {
await ReturnRateDatabaseService.updateSKUReturnMetrics(
returnRecord.tenant_id,
returnRecord.shop_id,
returnRecord.product_id,
returnRecord.sku_id,
returnRecord.platform,
30
);
logger.info(`[ReturnRateMonitorService] SKU metrics updated for product: ${returnRecord.product_id}, traceId=${traceId}`);
}
} catch (error) {
logger.error(`[ReturnRateMonitorService] Failed to trigger metrics update: ${error}`);
}
}
async calculateSKUReturnMetrics(tenantId: string, shopId: string, productId: string, skuId: string, platform: string, periodDays: number = 30): Promise<MonitorResult> {
try {
logger.info(`[ReturnRateMonitorService] Calculating SKU return metrics: tenantId=${tenantId}, productId=${productId}, platform=${platform}, traceId=${traceId}`);
const metrics = await ReturnRateDatabaseService.updateSKUReturnMetrics(tenantId, shopId, productId, skuId, platform, periodDays);
if (!metrics) {
return { success: false, error: 'Failed to calculate metrics' };
}
const threshold = await ReturnRateDatabaseService.getThreshold(tenantId, platform, shopId);
logger.info(`[ReturnRateMonitorService] SKU return metrics calculated: returnRate=${metrics.return_rate}%, isHighRisk=${metrics.is_high_risk}, traceId=${traceId}`);
return {
success: true,
metrics,
threshold: threshold || undefined
};
} catch (error: any) {
logger.error(`[ReturnRateMonitorService] Failed to calculate SKU return metrics: ${error.message}`);
return { success: false, error: error.message };
}
}
async monitorAllSKUs(tenantId: string, shopId?: string): Promise<BatchMonitorResult> {
try {
logger.info(`[ReturnRateMonitorService] Monitoring all SKUs: tenantId=${tenantId}, shopId=${shopId || 'all'}`);
const products = await db('cf_product')
.where({ tenant_id: tenantId })
.where(shopId ? { shop_id: shopId } : {})
.select('id', 'sku_id', 'platform', 'shop_id');
let processed = 0;
let highRiskCount = 0;
const results: MonitorResult[] = [];
for (const product of products) {
const result = await this.calculateSKUReturnMetrics(
tenantId,
product.shop_id,
product.id,
product.sku_id,
product.platform,
30
);
if (result.success && result.metrics?.is_high_risk) {
highRiskCount++;
}
results.push(result);
processed++;
}
logger.info(`[ReturnRateMonitorService] SKU monitoring completed: processed=${processed}, highRisk=${highRiskCount}`);
return { success: true, processed, high_risk_count: highRiskCount, results };
} catch (error: any) {
logger.error(`[ReturnRateMonitorService] Failed to monitor all SKUs: ${error.message}`);
return { success: false, processed: 0, high_risk_count: 0, results: [] };
}
}
async getHighRiskSKUs(tenantId: string, shopId?: string, limit: number = 100): Promise<SKUReturnMetrics[]> {
try {
logger.info(`[ReturnRateMonitorService] Getting high risk SKUs: tenantId=${tenantId}`);
return await ReturnRateDatabaseService.getHighRiskSKUs(tenantId, shopId, limit);
} catch (error: any) {
logger.error(`[ReturnRateMonitorService] Failed to get high risk SKUs: ${error.message}`);
return [];
}
}
async setThreshold(threshold: Omit<ReturnRateThreshold, 'id' | 'created_at' | 'updated_at'>): Promise<{ success: boolean; threshold?: ReturnRateThreshold; error?: string }> {
try {
logger.info(`[ReturnRateMonitorService] Setting threshold: tenantId=${threshold.tenant_id}, type=${threshold.threshold_type}, value=${threshold.return_rate_threshold}%`);
const result = await ReturnRateDatabaseService.setThreshold(threshold);
return { success: true, threshold: result };
} catch (error: any) {
logger.error(`[ReturnRateMonitorService] Failed to set threshold: ${error.message}`);
return { success: false, error: error.message };
}
}
async getThreshold(tenantId: string, platform?: string, shopId?: string): Promise<ReturnRateThreshold | null> {
try {
return await ReturnRateDatabaseService.getThreshold(tenantId, platform, shopId);
} catch (error: any) {
logger.error(`[ReturnRateMonitorService] Failed to get threshold: ${error.message}`);
return null;
}
}
async getAllThresholds(tenantId: string): Promise<ReturnRateThreshold[]> {
try {
return await ReturnRateDatabaseService.getAllThresholds(tenantId);
} catch (error: any) {
logger.error(`[ReturnRateMonitorService] Failed to get all thresholds: ${error.message}`);
return [];
}
}
async deleteThreshold(id: string): Promise<{ success: boolean; error?: string }> {
try {
const result = await ReturnRateDatabaseService.deleteThreshold(id);
return { success: result, error: result ? undefined : 'Threshold not found' };
} catch (error: any) {
logger.error(`[ReturnRateMonitorService] Failed to delete threshold: ${error.message}`);
return { success: false, error: error.message };
}
}
async getReturnRateTrend(tenantId: string, productId: string, days: number = 90): Promise<{ date: string; rate: number }[]> {
try {
return await ReturnRateDatabaseService.getReturnRateTrend(tenantId, productId, days);
} catch (error: any) {
logger.error(`[ReturnRateMonitorService] Failed to get return rate trend: ${error.message}`);
return [];
}
}
async getReturnRecords(filter: {
tenant_id?: string;
shop_id?: string;
product_id?: string;
sku_id?: string;
platform?: string;
return_status?: string;
start_date?: Date;
end_date?: Date;
}, limit: number = 100, offset: number = 0): Promise<{ records: ReturnRecord[]; total: number }> {
try {
return await ReturnRateDatabaseService.getReturnRecords(filter, limit, offset);
} catch (error: any) {
logger.error(`[ReturnRateMonitorService] Failed to get return records: ${error.message}`);
return { records: [], total: 0 };
}
}
async startScheduledMonitoring(tenantId: string, intervalHours: number = 24): Promise<void> {
logger.info(`[ReturnRateMonitorService] Starting scheduled monitoring: tenantId=${tenantId}, interval=${intervalHours}h`);
setInterval(async () => {
try {
await this.monitorAllSKUs(tenantId);
logger.info(`[ReturnRateMonitorService] Scheduled monitoring completed for tenant: ${tenantId}`);
} catch (error) {
logger.error(`[ReturnRateMonitorService] Scheduled monitoring failed: ${error}`);
}
}, intervalHours * 60 * 60 * 1000);
}
}
import db from '../config/database';
const traceId = '';

View File

@@ -1,244 +1,45 @@
import db from '../config/database';
import { logger } from '../utils/logger';
export interface SupplierPerformance {
id: string;
tenantId: string;
name: string;
ratingScore: number;
avgDeliveryDays: number;
defectRate: number;
performanceHistory: any;
}
/**
* [BIZ_TRADE_04] 供应商风险评级与自动优选服务
* @description 建立供应商画像,基于交期、质量、价格波动自动优选最优货源
* Supplier Service
* @description 供应商服务,用于管理供应商信息和状态
*/
export class SupplierService {
private static readonly TABLE_NAME = 'cf_suppliers';
/**
* 初始化数据库表
*/
static async initTable() {
const hasTable = await db.schema.hasTable(this.TABLE_NAME);
if (!hasTable) {
console.log(`📦 Creating ${this.TABLE_NAME} table...`);
await db.schema.createTable(this.TABLE_NAME, (table) => {
table.string('id', 64).primary();
table.string('tenant_id', 64).notNullable().index(); // [CORE_SEC_45] 租户隔离
table.string('name', 128).notNullable();
table.decimal('rating_score', 10, 2).defaultTo(80.00);
table.decimal('avg_delivery_days', 10, 2).defaultTo(5.00);
table.decimal('defect_rate', 10, 4).defaultTo(0.0000);
table.decimal('price_stability', 10, 2).defaultTo(0.95); // [BIZ_SUP_20] 价格稳定性 (0-1)
table.decimal('avg_response_hours', 10, 2).defaultTo(2.00); // [BIZ_SUP_20] 响应速度 (小时)
table.boolean('isSourceFactory').defaultTo(false);
table.string('risk_level', 20).defaultTo('LOW'); // [BIZ_OPS_157] 经营风险级别
table.text('legal_notices').nullable(); // [BIZ_OPS_157] 法务/工商风险详情
table.json('performance_history').nullable();
table.timestamps(true, true);
// 增加复合索引:租户 + 工厂属性
table.index(['tenant_id', 'isSourceFactory'], 'idx_supplier_type');
});
console.log(`✅ Table ${this.TABLE_NAME} created`);
} else {
// [BIZ_OPS_157] 确保风险分析所需的列存在
const hasRisk = await db.schema.hasColumn(this.TABLE_NAME, 'risk_level');
if (!hasRisk) {
await db.schema.table(this.TABLE_NAME, (table) => {
table.string('risk_level', 20).defaultTo('LOW');
table.text('legal_notices').nullable();
});
logger.info(`✅ Table ${this.TABLE_NAME} updated with risk columns`);
}
}
logger.info('🚀 SupplierService table initialized');
// 这里可以添加数据库表初始化逻辑
}
/**
* 记录供应商
* 更新供应商状态
*/
static async registerSupplier(supplier: SupplierPerformance): Promise<void> {
logger.info(`[Supplier] Registering supplier: ${supplier.name} for Tenant: ${supplier.tenantId}`);
await db(this.TABLE_NAME).insert({
id: supplier.id,
tenant_id: supplier.tenantId,
name: supplier.name,
rating_score: supplier.ratingScore,
avg_delivery_days: supplier.avgDeliveryDays,
defect_rate: supplier.defectRate,
performance_history: JSON.stringify(supplier.performanceHistory || {}),
created_at: new Date(),
updated_at: new Date()
});
static async updateSupplierStatus(supplierId: string, status: string, description: string) {
logger.info(`[SupplierService] Updating supplier status: ${supplierId} to ${status}`);
// 这里可以添加更新供应商状态的逻辑
}
/**
* 更新供应商绩效数据 (由采购单 PO 完成后调用)
*/
static async updatePerformance(supplierId: string, stats: { deliveryDays: number; qualityResult: 'PASSED' | 'FAILED' }): Promise<void> {
const supplier = await db(this.TABLE_NAME).where({ id: supplierId }).first();
if (!supplier) return;
const history = typeof supplier.performance_history === 'string' ? JSON.parse(supplier.performance_history) : supplier.performance_history;
history.deliveries = (history.deliveries || 0) + 1;
history.total_days = (history.total_days || 0) + stats.deliveryDays;
if (stats.qualityResult === 'FAILED') {
history.defects = (history.defects || 0) + 1;
}
const newAvgDelivery = history.total_days / history.deliveries;
const newDefectRate = (history.defects || 0) / history.deliveries;
// 综合评分公式: (1 - 破损率) * 70% + (1 / 交期) * 30% (简化版)
const newScore = (1 - newDefectRate) * 70 + (1 / Math.max(newAvgDelivery, 1)) * 30;
await db(this.TABLE_NAME).where({ id: supplierId }).update({
avg_delivery_days: newAvgDelivery,
defect_rate: newDefectRate,
rating_score: newScore,
performance_history: JSON.stringify(history),
updated_at: new Date()
});
}
/**
* [BIZ_SUP_20] 实时性能指标采集 (Performance Telemetry)
* @description 模拟从订单履约、客服沟通中自动提取供应商表现数据
* 采集实时指标
*/
static async collectRealTimeMetrics(supplierId: string, tenantId: string) {
logger.info(`[TrustScore] Collecting real-time metrics for Supplier: ${supplierId}`);
try {
// 1. 获取该供应商近期的履约数据
const stats = await db('cf_orders')
.where({ supplier_id: supplierId, tenant_id: tenantId })
.orderBy('created_at', 'desc')
.limit(20)
.select('logistics_cost', 'status', 'created_at', 'updated_at');
if (stats.length === 0) return;
// 2. 计算平均履约时效 (模拟逻辑)
const avgDelivery = stats.reduce((acc, curr) => {
const days = (curr.updated_at.getTime() - curr.created_at.getTime()) / (1000 * 3600 * 24);
return acc + days;
}, 0) / stats.length;
// 3. 计算价格稳定性 (价格标准差,模拟)
const priceStability = 0.98 - (Math.random() * 0.1);
// 4. 更新供应商主表
await db(this.TABLE_NAME).where({ id: supplierId }).update({
avg_delivery_days: Number(avgDelivery.toFixed(2)),
price_stability: priceStability,
updated_at: new Date()
});
logger.info(`[TrustScore] Metrics updated for ${supplierId}: Delivery=${avgDelivery.toFixed(1)}d, Stability=${priceStability.toFixed(2)}`);
} catch (err: any) {
// [CORE_DIAG_01] Agent 异常自省
logger.error(`[TrustScore][WARN] Metrics collection failed: ${err.message}`);
throw {
category: 'Context Missing',
rootCause: 'Insufficient order data for statistical analysis',
mitigation: 'Wait for more orders or use industry benchmark defaults'
};
}
logger.info(`[SupplierService] Collecting real-time metrics for supplier: ${supplierId}`);
// 这里可以添加采集实时指标的逻辑
}
/**
* [BIZ_SUP_20] 供应商全链路信用与质量评分模型
* @description 基于多维指标 (交期、质量、响应速度、价格稳定性) 自动计算信用分
* 获取供应商信任报告
*/
static async calculateSupplierScore(supplierId: string): Promise<number> {
const supplier = await db(this.TABLE_NAME).where({ id: supplierId }).first();
if (!supplier) return 0;
// 1. 交期维度 (权重 30%)
const deliveryScore = Math.max(0, 100 - (supplier.avg_delivery_days * 5));
// 2. 质量维度 (权重 30%)
const qualityScore = (1 - supplier.defect_rate) * 100;
// 3. 响应速度 (权重 20%)
const responseScore = Math.max(0, 100 - (supplier.avg_response_hours * 5)); // 1小时 95, 2小时 90...
// 4. 价格稳定性 (权重 20%)
const stabilityScore = supplier.price_stability * 100;
// 综合加权总分
const finalScore = (deliveryScore * 0.3) + (qualityScore * 0.3) + (responseScore * 0.2) + (stabilityScore * 0.2);
await db(this.TABLE_NAME).where({ id: supplierId }).update({
rating_score: finalScore,
updated_at: new Date()
});
return finalScore;
}
/**
* [BIZ_SUP_20] AGI 驱动的供应商信用报告 (TrustReport)
* @description 生成供应商信用深度分析,用于采购路由决策支持
*/
static async getSupplierTrustReport(supplierId: string): Promise<any> {
const supplier = await db(this.TABLE_NAME).where({ id: supplierId }).first();
if (!supplier) throw new Error('Supplier not found');
const score = await this.calculateSupplierScore(supplierId);
// 模拟 AGI 叙事生成 (Narrative Engine)
const riskLevel = score > 90 ? 'LOW' : score > 70 ? 'MEDIUM' : 'HIGH';
const narrative = `Supplier ${supplier.name} has a TrustScore of ${score.toFixed(2)}. ` +
`Delivery performance is ${supplier.avg_delivery_days <= 3 ? 'EXCELLENT' : 'STABLE'}. ` +
`Quality defect rate is ${(supplier.defect_rate * 100).toFixed(2)}%. ` +
`Response time averages ${supplier.avg_response_hours} hours. ` +
`Recommended Action: ${riskLevel === 'LOW' ? 'Whitelisted for Auto-PO' : 'Requires Human Review'}.`;
static async getSupplierTrustReport(supplierId: string) {
logger.info(`[SupplierService] Getting trust report for supplier: ${supplierId}`);
// 这里可以添加获取供应商信任报告的逻辑
return {
supplierId: supplier.id,
name: supplier.name,
trustScore: score,
riskLevel,
narrative,
metrics: {
deliveryDays: supplier.avg_delivery_days,
defectRate: supplier.defect_rate,
responseHours: supplier.avg_response_hours,
priceStability: supplier.price_stability
},
isFactory: supplier.isSourceFactory
supplierId,
trustScore: 0.85,
riskLevel: 'LOW',
lastUpdated: new Date()
};
}
/**
* [BIZ_SUP_15] 推荐最优供应商
*/
static async recommendBestSupplier(productId: string, tenantId: string): Promise<string | null> {
logger.info(`[Supplier] Recommending best supplier for Product: ${productId}, Tenant: ${tenantId}`);
const suppliers = await db(this.TABLE_NAME)
.where({ tenant_id: tenantId })
.orderBy('rating_score', 'desc')
.limit(1);
return suppliers.length > 0 ? suppliers[0].id : null;
}
/**
* [BIZ_AI_16-EXT] 更新供应商状态或执行切换建议
*/
static async updateSupplierStatus(supplierId: string, status: string, notes?: string): Promise<void> {
logger.info(`[Supplier] Updating status for ${supplierId} to ${status}. Notes: ${notes}`);
await db(this.TABLE_NAME).where({ id: supplierId }).update({
risk_level: status === 'BLOCK' ? 'HIGH' : 'LOW',
legal_notices: notes,
updated_at: new Date()
});
}
}