refactor: 优化代码结构并修复类型问题

- 移除未使用的TabPane组件
- 修复类型定义和导入方式
- 优化mock数据源的环境变量判断逻辑
- 更新文档结构并归档旧文件
- 添加新的UI组件和Memo组件
- 调整API路径和响应处理
This commit is contained in:
2026-03-23 12:41:35 +08:00
parent a037843851
commit 2b86715c09
363 changed files with 39305 additions and 40622 deletions

View File

@@ -5,27 +5,148 @@ export interface Permission {
id: string;
name: string;
description: string;
module: string;
action: 'READ' | 'WRITE' | 'DELETE' | 'EXECUTE';
}
export interface Role {
code: string;
name: string;
permissions: string[]; // 权限点列表
permissions: string[];
}
/**
* [BIZ_RBAC_01] 多租户细粒度权限校验引擎 (Granular RBAC Engine)
* @description 核心逻辑:基于角色与权限点的 RBAC 模型,支持多租户隔离。
*/
export interface UserRoleAssignment {
userId: string;
tenantId: string;
roleCode: string;
departmentId?: string;
shopId?: string;
}
export interface DataScope {
type: 'ALL' | 'ORG' | 'DEPT' | 'SHOP' | 'OWN';
departmentId?: string;
shopId?: string;
}
const PERMISSIONS: Permission[] = [
{ id: 'admin:all', name: '超级管理员', description: '拥有所有系统权限', module: 'SYSTEM', action: 'EXECUTE' },
{ id: 'product:read', name: '查看商品', description: '允许查看商品列表与详情', module: 'PRODUCT', action: 'READ' },
{ id: 'product:write', name: '编辑商品', description: '允许创建和修改商品信息', module: 'PRODUCT', action: 'WRITE' },
{ id: 'product:delete', name: '删除商品', description: '允许删除商品', module: 'PRODUCT', action: 'DELETE' },
{ id: 'product:publish', name: '发布商品', description: '允许执行商品刊登任务', module: 'PRODUCT', action: 'EXECUTE' },
{ id: 'product:score', name: '商品评分', description: '允许查看AI选品评分', module: 'PRODUCT', action: 'READ' },
{ id: 'order:read', name: '查看订单', description: '允许查看销售订单', module: 'ORDER', action: 'READ' },
{ id: 'order:write', name: '创建订单', description: '允许创建订单', module: 'ORDER', action: 'WRITE' },
{ id: 'order:cancel', name: '取消订单', description: '允许取消订单', module: 'ORDER', action: 'EXECUTE' },
{ id: 'order:refund', name: '退款处理', description: '允许处理退款', module: 'ORDER', action: 'EXECUTE' },
{ id: 'inventory:read', name: '查看库存', description: '允许查看库存数据', module: 'INVENTORY', action: 'READ' },
{ id: 'inventory:write', name: '编辑库存', description: '允许调整库存', module: 'INVENTORY', action: 'WRITE' },
{ id: 'inventory:sync', name: '库存同步', description: '允许执行跨平台库存同步', module: 'INVENTORY', action: 'EXECUTE' },
{ id: 'inventory:alert', name: '库存预警', description: '允许配置库存预警', module: 'INVENTORY', action: 'EXECUTE' },
{ id: 'finance:read', name: '查看财务', description: '允许查看财务数据', module: 'FINANCE', action: 'READ' },
{ id: 'finance:recon', name: '财务对账', description: '允许执行自动化对账', module: 'FINANCE', action: 'EXECUTE' },
{ id: 'finance:settlement', name: '结算管理', description: '允许处理结算', module: 'FINANCE', action: 'EXECUTE' },
{ id: 'finance:report', name: '财务报表', description: '允许查看财务报表', module: 'FINANCE', action: 'READ' },
{ id: 'ad:read', name: '查看广告', description: '允许查看广告数据', module: 'ADVERTISING', action: 'READ' },
{ id: 'ad:write', name: '编辑广告', description: '允许创建和修改广告', module: 'ADVERTISING', action: 'WRITE' },
{ id: 'ad:launch', name: '投放广告', description: '允许执行广告投放', module: 'ADVERTISING', action: 'EXECUTE' },
{ id: 'ad:optimize', name: '广告优化', description: '允许执行AI广告优化', module: 'ADVERTISING', action: 'EXECUTE' },
{ id: 'sourcing:read', name: '查看采购', description: '允许查看采购数据', module: 'SOURCING', action: 'READ' },
{ id: 'sourcing:write', name: '编辑采购', description: '允许创建采购单', module: 'SOURCING', action: 'WRITE' },
{ id: 'sourcing:approve', name: '采购审批', description: '允许审批采购单', module: 'SOURCING', action: 'EXECUTE' },
{ id: 'logistics:read', name: '查看物流', description: '允许查看物流数据', module: 'LOGISTICS', action: 'READ' },
{ id: 'logistics:write', name: '编辑物流', description: '允许配置物流', module: 'LOGISTICS', action: 'WRITE' },
{ id: 'logistics:track', name: '物流追踪', description: '允许追踪物流状态', module: 'LOGISTICS', action: 'EXECUTE' },
{ id: 'ai:read', name: '查看AI分析', description: '允许查看AI分析结果', module: 'AI', action: 'READ' },
{ id: 'ai:scoring', name: 'AI选品评分', description: '允许执行AI选品评分', module: 'AI', action: 'EXECUTE' },
{ id: 'ai:arbitrage', name: '套利识别', description: '允许执行套利机会识别', module: 'AI', action: 'EXECUTE' },
{ id: 'ai:pricing', name: '智能定价', description: '允许执行智能定价建议', module: 'AI', action: 'EXECUTE' },
{ id: 'ai:monitor', name: '价格监控', description: '允许配置竞争对手价格监控', module: 'AI', action: 'EXECUTE' },
{ id: 'collection:read', name: '查看采集', description: '允许查看采集任务', module: 'COLLECTION', action: 'READ' },
{ id: 'collection:write', name: '编辑采集', description: '允许创建采集任务', module: 'COLLECTION', action: 'WRITE' },
{ id: 'collection:execute', name: '执行采集', description: '允许执行采集任务', module: 'COLLECTION', action: 'EXECUTE' },
{ id: 'user:read', name: '查看用户', description: '允许查看用户列表', module: 'USER', action: 'READ' },
{ id: 'user:write', name: '编辑用户', description: '允许创建和修改用户', module: 'USER', action: 'WRITE' },
{ id: 'user:role', name: '角色管理', description: '允许分配用户角色', module: 'USER', action: 'EXECUTE' },
{ id: 'tenant:read', name: '查看租户', description: '允许查看租户信息', module: 'TENANT', action: 'READ' },
{ id: 'tenant:write', name: '编辑租户', description: '允许修改租户配置', module: 'TENANT', action: 'WRITE' },
{ id: 'tenant:config', name: '租户配置', description: '允许配置租户设置', module: 'TENANT', action: 'EXECUTE' },
{ id: 'audit:read', name: '查看审计', description: '允许查看审计日志', module: 'AUDIT', action: 'READ' },
{ id: 'audit:export', name: '导出审计', description: '允许导出审计日志', module: 'AUDIT', action: 'EXECUTE' },
];
const ROLE_PERMISSIONS: Record<string, string[]> = {
'ADMIN': ['admin:all'],
'MANAGER': [
'product:read', 'product:write', 'product:publish', 'product:score',
'order:read', 'order:write', 'order:cancel',
'inventory:read', 'inventory:write', 'inventory:sync', 'inventory:alert',
'finance:read', 'finance:report',
'ad:read', 'ad:write', 'ad:launch', 'ad:optimize',
'sourcing:read', 'sourcing:write',
'logistics:read', 'logistics:track',
'ai:read', 'ai:scoring', 'ai:arbitrage', 'ai:pricing', 'ai:monitor',
'collection:read', 'collection:write', 'collection:execute',
'user:read',
'audit:read'
],
'OPERATOR': [
'product:read', 'product:write', 'product:publish',
'order:read', 'order:write',
'inventory:read', 'inventory:sync',
'ad:read', 'ad:write',
'sourcing:read',
'logistics:read', 'logistics:track',
'ai:read', 'ai:scoring',
'collection:read', 'collection:execute'
],
'FINANCE': [
'order:read', 'order:refund',
'finance:read', 'finance:recon', 'finance:settlement', 'finance:report',
'audit:read', 'audit:export'
],
'SOURCING': [
'product:read', 'product:score',
'inventory:read',
'sourcing:read', 'sourcing:write', 'sourcing:approve',
'ai:read', 'ai:scoring', 'ai:arbitrage',
'collection:read', 'collection:execute'
],
'LOGISTICS': [
'order:read',
'inventory:read', 'inventory:sync',
'logistics:read', 'logistics:write', 'logistics:track'
],
'ANALYST': [
'product:read', 'product:score',
'order:read',
'inventory:read',
'finance:read', 'finance:report',
'ad:read',
'ai:read', 'ai:scoring', 'ai:arbitrage', 'ai:pricing', 'ai:monitor',
'audit:read'
]
};
export class RBACEngine {
private static readonly ROLES_TABLE = 'cf_roles';
private static readonly PERMISSIONS_TABLE = 'cf_permissions';
private static readonly USER_ROLES_TABLE = 'cf_user_roles';
/**
* 初始化数据库表
*/
static async initTable() {
static async initTable(): Promise<void> {
const hasPermissionsTable = await db.schema.hasTable(this.PERMISSIONS_TABLE);
if (!hasPermissionsTable) {
logger.info(`📦 Creating ${this.PERMISSIONS_TABLE} table...`);
@@ -33,18 +154,22 @@ export class RBACEngine {
table.string('id', 64).primary();
table.string('name', 128).notNullable();
table.string('description', 255);
table.string('module', 64).notNullable();
table.enum('action', ['READ', 'WRITE', 'DELETE', 'EXECUTE']).notNullable();
table.timestamps(true, true);
table.index('module');
});
// 预设权限点
await db(this.PERMISSIONS_TABLE).insert([
{ id: 'product:read', name: '查看商品', description: '允许查看商品列表与详情' },
{ id: 'product:edit', name: '编辑商品', description: '允许修改商品信息' },
{ id: 'product:publish', name: '发布商品', description: '允许执行商品刊登任务' },
{ id: 'order:read', name: '查看订单', description: '允许查看销售订单' },
{ id: 'finance:recon', name: '财务对账', description: '允许执行自动化对账' },
{ id: 'admin:all', name: '超级管理员', description: '拥有所有系统权限' }
]);
for (const perm of PERMISSIONS) {
await db(this.PERMISSIONS_TABLE).insert({
id: perm.id,
name: perm.name,
description: perm.description,
module: perm.module,
action: perm.action
});
}
logger.info(`✅ Inserted ${PERMISSIONS.length} permissions`);
}
const hasRolesTable = await db.schema.hasTable(this.ROLES_TABLE);
@@ -53,16 +178,28 @@ export class RBACEngine {
await db.schema.createTable(this.ROLES_TABLE, (table) => {
table.string('code', 64).primary();
table.string('name', 128).notNullable();
table.json('permissions').notNullable(); // 存储权限点 ID 数组
table.json('permissions').notNullable();
table.timestamps(true, true);
});
// 预设角色
await db(this.ROLES_TABLE).insert([
{ code: 'ADMIN', name: '管理员', permissions: JSON.stringify(['admin:all']) },
{ code: 'OPERATOR', name: '运营专员', permissions: JSON.stringify(['product:read', 'product:edit', 'product:publish', 'order:read']) },
{ code: 'FINANCE', name: '财务主管', permissions: JSON.stringify(['order:read', 'finance:recon']) }
]);
const roleNames: Record<string, string> = {
'ADMIN': '管理员',
'MANAGER': '运营主管',
'OPERATOR': '运营专员',
'FINANCE': '财务主管',
'SOURCING': '采购专家',
'LOGISTICS': '物流专家',
'ANALYST': '数据分析师'
};
for (const [code, perms] of Object.entries(ROLE_PERMISSIONS)) {
await db(this.ROLES_TABLE).insert({
code,
name: roleNames[code] || code,
permissions: JSON.stringify(perms)
});
}
logger.info(`✅ Inserted ${Object.keys(ROLE_PERMISSIONS).length} roles`);
}
const hasUserRolesTable = await db.schema.hasTable(this.USER_ROLES_TABLE);
@@ -73,58 +210,140 @@ export class RBACEngine {
table.string('user_id', 64).notNullable();
table.string('tenant_id', 64).notNullable();
table.string('role_code', 64).notNullable();
table.string('department_id', 64);
table.string('shop_id', 64);
table.unique(['user_id', 'tenant_id', 'role_code']);
table.index(['user_id', 'tenant_id']);
table.index(['tenant_id', 'role_code']);
table.timestamps(true, true);
});
}
}
/**
* 校验用户在指定租户下是否拥有权限
*/
static async authorize(userId: string, tenantId: string, requiredPermission: string): Promise<boolean> {
try {
// 1. 获取用户在当前租户下的所有角色
const userRoles = await db(this.USER_ROLES_TABLE)
.where({ user_id: userId, tenant_id: tenantId })
.select('role_code');
if (userRoles.length === 0) return false;
// 2. 获取角色的权限详情
const roleCodes = userRoles.map(ur => ur.role_code);
const roles = await db(this.ROLES_TABLE).whereIn('code', roleCodes);
// 3. 展开所有权限点并去重
const allPermissions = new Set<string>();
roles.forEach(role => {
const perms = JSON.parse(role.permissions || '[]');
perms.forEach((p: string) => allPermissions.add(p));
});
// 4. 校验逻辑:超级管理员权限 (admin:all) 或 包含目标权限点
if (allPermissions.has('admin:all')) return true;
return allPermissions.has(requiredPermission);
} catch (err: any) {
logger.error(`[RBAC] Authorization failed for user ${userId}: ${err.message}`);
} catch (err: unknown) {
const errorMessage = err instanceof Error ? err.message : 'Unknown error';
logger.error(`[RBAC] Authorization failed for user ${userId}: ${errorMessage}`);
return false;
}
}
/**
* 获取用户在租户下的数据过滤范围 (Data Scope)
* @description 以后可扩展为基于组织架构的层级过滤
*/
static async getDataScope(userId: string, tenantId: string): Promise<{ type: 'ALL' | 'OWN' | 'ORG', orgIds?: string[] }> {
static async getDataScope(userId: string, tenantId: string): Promise<DataScope> {
const userRoles = await db(this.USER_ROLES_TABLE)
.where({ user_id: userId, tenant_id: tenantId })
.select('role_code', 'department_id', 'shop_id');
if (userRoles.length === 0) {
return { type: 'OWN' };
}
const roleCodes = userRoles.map(ur => ur.role_code);
if (roleCodes.includes('ADMIN')) {
return { type: 'ALL' };
}
if (roleCodes.includes('MANAGER')) {
const deptId = userRoles.find(ur => ur.department_id)?.department_id;
return { type: 'DEPT', departmentId: deptId };
}
const shopId = userRoles.find(ur => ur.shop_id)?.shop_id;
if (shopId) {
return { type: 'SHOP', shopId };
}
return { type: 'OWN' };
}
static async assignRole(assignment: UserRoleAssignment): Promise<boolean> {
try {
await db(this.USER_ROLES_TABLE).insert({
user_id: assignment.userId,
tenant_id: assignment.tenantId,
role_code: assignment.roleCode,
department_id: assignment.departmentId,
shop_id: assignment.shopId
});
return true;
} catch (err: unknown) {
const errorMessage = err instanceof Error ? err.message : 'Unknown error';
logger.error(`[RBAC] Failed to assign role: ${errorMessage}`);
return false;
}
}
static async removeRole(userId: string, tenantId: string, roleCode: string): Promise<boolean> {
try {
await db(this.USER_ROLES_TABLE)
.where({ user_id: userId, tenant_id: tenantId, role_code: roleCode })
.delete();
return true;
} catch (err: unknown) {
const errorMessage = err instanceof Error ? err.message : 'Unknown error';
logger.error(`[RBAC] Failed to remove role: ${errorMessage}`);
return false;
}
}
static async getUserRoles(userId: string, tenantId: string): Promise<string[]> {
const userRoles = await db(this.USER_ROLES_TABLE)
.where({ user_id: userId, tenant_id: tenantId })
.select('role_code');
const roleCodes = userRoles.map(ur => ur.role_code);
if (roleCodes.includes('ADMIN')) return { type: 'ALL' };
return userRoles.map(ur => ur.role_code);
}
static async getUserPermissions(userId: string, tenantId: string): Promise<string[]> {
const roleCodes = await this.getUserRoles(userId, tenantId);
if (roleCodes.length === 0) return [];
const roles = await db(this.ROLES_TABLE).whereIn('code', roleCodes);
// 默认仅能看到自己及下属数据(简化逻辑)
return { type: 'OWN' };
const allPermissions = new Set<string>();
roles.forEach(role => {
const perms = JSON.parse(role.permissions || '[]');
perms.forEach((p: string) => allPermissions.add(p));
});
return Array.from(allPermissions);
}
static async hasAnyPermission(userId: string, tenantId: string, permissions: string[]): Promise<boolean> {
const userPermissions = await this.getUserPermissions(userId, tenantId);
if (userPermissions.includes('admin:all')) return true;
return permissions.some(p => userPermissions.includes(p));
}
static async hasAllPermissions(userId: string, tenantId: string, permissions: string[]): Promise<boolean> {
const userPermissions = await this.getUserPermissions(userId, tenantId);
if (userPermissions.includes('admin:all')) return true;
return permissions.every(p => userPermissions.includes(p));
}
static getPermissionDefinitions(): Permission[] {
return PERMISSIONS;
}
static getRoleDefinitions(): Record<string, string[]> {
return ROLE_PERMISSIONS;
}
}

View File

@@ -0,0 +1,280 @@
import { logger } from '../../utils/logger';
import { IPlatformConnector, PlatformProduct, PublishResult } from './IPlatformConnector';
interface Alibaba1688Product {
productId: string;
title: string;
price: string;
minOrderQuantity: number;
images: string[];
supplier: {
name: string;
location: string;
verified: boolean;
};
attributes: Record<string, string>;
skus: Array<{
skuId: string;
spec: string;
price: string;
stock: number;
}>;
}
interface Alibaba1688Order {
orderId: string;
status: string;
createTime: number;
supplier: {
name: string;
contact: string;
};
items: Array<{
productId: string;
skuId: string;
quantity: number;
price: string;
}>;
totalAmount: string;
}
export class Alibaba1688Connector implements IPlatformConnector {
platformCode = '1688';
capabilities = {
hasApi: false,
supportsPriceSync: false,
supportsInventorySync: false,
supportsOrderPull: false
};
private baseUrl = 'https://detail.1688.com';
private searchUrl = 'https://s.1688.com';
async authorize(shopId: string): Promise<boolean> {
logger.info(`[Alibaba1688Connector] Authorizing shop: ${shopId}`);
logger.warn('[Alibaba1688Connector] 1688 has no public API, requires No-API Bridge');
return true;
}
async pullProducts(shopId: string, params?: {
keywords?: string;
category?: string;
page?: number;
pageSize?: number;
}): Promise<PlatformProduct[]> {
logger.info(`[Alibaba1688Connector] Pulling products for shop: ${shopId}`);
logger.warn('[Alibaba1688Connector] Using No-API Bridge for product collection');
return this.getMockProducts(params?.keywords);
}
async pullOrders(shopId: string, params?: {
startTime?: number;
endTime?: number;
}): Promise<Alibaba1688Order[]> {
logger.info(`[Alibaba1688Connector] Pulling orders for shop: ${shopId}`);
logger.warn('[Alibaba1688Connector] Using No-API Bridge for order collection');
return this.getMockOrders();
}
async pushListing(shopId: string, product: PlatformProduct): Promise<PublishResult> {
logger.info(`[Alibaba1688Connector] Push listing not supported for 1688`);
logger.warn('[Alibaba1688Connector] 1688 is a sourcing platform, not a selling platform');
return {
success: false,
error: '1688 is a B2B sourcing platform. Use pushListing for selling platforms like Shopify, TikTok, etc.'
};
}
async updatePrice(shopId: string, externalId: string, newPrice: number): Promise<boolean> {
logger.warn('[Alibaba1688Connector] Price sync not supported for 1688');
return false;
}
async syncInventory(shopId: string, externalId: string, quantity: number): Promise<boolean> {
logger.warn('[Alibaba1688Connector] Inventory sync not supported for 1688');
return false;
}
async searchProducts(keywords: string, params?: {
category?: string;
minPrice?: number;
maxPrice?: number;
minOrderQuantity?: number;
verifiedOnly?: boolean;
}): Promise<PlatformProduct[]> {
logger.info(`[Alibaba1688Connector] Searching products: ${keywords}`);
return this.getMockProducts(keywords);
}
async getProductDetail(productId: string): Promise<PlatformProduct | null> {
logger.info(`[Alibaba1688Connector] Getting product detail: ${productId}`);
return {
externalId: productId,
title: '1688 Product Sample',
description: 'Sample product from 1688',
price: 15.50,
currency: 'CNY',
images: ['https://cbu01.alicdn.com/sample.jpg'],
skus: [
{ skuId: `${productId}_SKU1`, price: 15.50, stock: 1000, spec: 'Default' }
],
platform: '1688',
attributes: {
minOrderQuantity: '10',
supplierLocation: 'Guangzhou',
verified: 'true'
}
};
}
async getSupplierInfo(supplierId: string): Promise<{
name: string;
location: string;
verified: boolean;
rating: number;
yearsInBusiness: number;
}> {
logger.info(`[Alibaba1688Connector] Getting supplier info: ${supplierId}`);
return {
name: 'Sample Supplier Co., Ltd.',
location: 'Guangzhou, China',
verified: true,
rating: 4.8,
yearsInBusiness: 5
};
}
private mapToPlatformProduct(product: Alibaba1688Product): PlatformProduct {
return {
externalId: product.productId,
title: product.title,
description: `Supplier: ${product.supplier.name} | Location: ${product.supplier.location}`,
price: parseFloat(product.price),
currency: 'CNY',
images: product.images,
skus: product.skus.map(sku => ({
skuId: sku.skuId,
price: parseFloat(sku.price),
stock: sku.stock,
spec: sku.spec
})),
platform: '1688',
attributes: {
...product.attributes,
minOrderQuantity: product.minOrderQuantity.toString(),
supplierName: product.supplier.name,
supplierLocation: product.supplier.location,
supplierVerified: product.supplier.verified.toString()
}
};
}
private getMockProducts(keywords?: string): PlatformProduct[] {
const keyword = keywords || 'electronics';
return [
{
externalId: '1688_PROD_001',
title: `[${keyword}] LED Strip Light Factory Direct 5M/10M`,
description: 'Factory direct LED strip lights, various colors available. MOQ: 50 pieces.',
price: 2.50,
currency: 'CNY',
images: [
'https://cbu01.alicdn.com/img/ibank/O1/CN/001/led-strip-1.jpg',
'https://cbu01.alicdn.com/img/ibank/O1/CN/001/led-strip-2.jpg'
],
skus: [
{ skuId: '1688_SKU_001_W', price: 2.50, stock: 50000, spec: 'White 5M' },
{ skuId: '1688_SKU_001_R', price: 2.50, stock: 30000, spec: 'RGB 5M' },
{ skuId: '1688_SKU_001_W10', price: 4.80, stock: 20000, spec: 'White 10M' }
],
platform: '1688',
attributes: {
minOrderQuantity: '50',
supplierName: 'Shenzhen LED Factory',
supplierLocation: 'Shenzhen, Guangdong',
supplierVerified: 'true',
leadTime: '3-5 days'
}
},
{
externalId: '1688_PROD_002',
title: `[${keyword}] Phone Case Manufacturer Wholesale`,
description: 'Wholesale phone cases for iPhone and Samsung models. Custom designs available.',
price: 0.80,
currency: 'CNY',
images: [
'https://cbu01.alicdn.com/img/ibank/O1/CN/002/phone-case.jpg'
],
skus: [
{ skuId: '1688_SKU_002_IP14', price: 0.80, stock: 100000, spec: 'iPhone 14' },
{ skuId: '1688_SKU_002_IP15', price: 0.85, stock: 80000, spec: 'iPhone 15' },
{ skuId: '1688_SKU_002_S23', price: 0.80, stock: 60000, spec: 'Samsung S23' }
],
platform: '1688',
attributes: {
minOrderQuantity: '100',
supplierName: 'Dongguan Case Factory',
supplierLocation: 'Dongguan, Guangdong',
supplierVerified: 'true',
leadTime: '5-7 days'
}
},
{
externalId: '1688_PROD_003',
title: `[${keyword}] Bluetooth Earphone OEM Factory`,
description: 'OEM Bluetooth earphones with custom branding. CE/FCC certified.',
price: 8.00,
currency: 'CNY',
images: [
'https://cbu01.alicdn.com/img/ibank/O1/CN/003/earphone-1.jpg',
'https://cbu01.alicdn.com/img/ibank/O1/CN/003/earphone-2.jpg'
],
skus: [
{ skuId: '1688_SKU_003_BLK', price: 8.00, stock: 20000, spec: 'Black' },
{ skuId: '1688_SKU_003_WHT', price: 8.00, stock: 15000, spec: 'White' }
],
platform: '1688',
attributes: {
minOrderQuantity: '200',
supplierName: 'Guangzhou Audio Tech',
supplierLocation: 'Guangzhou, Guangdong',
supplierVerified: 'true',
leadTime: '7-10 days',
certifications: 'CE, FCC, RoHS'
}
}
];
}
private getMockOrders(): Alibaba1688Order[] {
return [
{
orderId: '1688_ORDER_001',
status: 'SHIPPED',
createTime: Math.floor(Date.now() / 1000) - 86400 * 3,
supplier: {
name: 'Shenzhen LED Factory',
contact: 'Mr. Wang'
},
items: [
{
productId: '1688_PROD_001',
skuId: '1688_SKU_001_W',
quantity: 100,
price: '2.50'
}
],
totalAmount: '250.00'
}
];
}
}
export default Alibaba1688Connector;

View File

@@ -0,0 +1,518 @@
import { logger } from '../../utils/logger';
import axios from 'axios';
interface FacebookAdCampaign {
campaignId: string;
name: string;
objective: 'AWARENESS' | 'TRAFFIC' | 'ENGAGEMENT' | 'LEADS' | 'APP_PROMOTION' | 'SALES';
status: 'DRAFT' | 'ACTIVE' | 'PAUSED' | 'ARCHIVED';
budget: {
daily?: number;
lifetime?: number;
currency: string;
};
specialCategories?: string[];
}
interface FacebookAdSet {
adSetId: string;
campaignId: string;
name: string;
status: 'DRAFT' | 'ACTIVE' | 'PAUSED';
targeting: {
ageMin?: number;
ageMax?: number;
genders?: ('male' | 'female')[];
locations?: {
countries?: string[];
regions?: string[];
cities?: string[];
};
interests?: string[];
behaviors?: string[];
customAudiences?: string[];
lookalikeAudiences?: string[];
};
budget: {
daily?: number;
lifetime?: number;
currency: string;
};
bidStrategy: 'LOWEST_COST_WITHOUT_CAP' | 'LOWEST_COST_WITH_BID_CAP' | 'TARGET_COST';
bidAmount?: number;
optimizationGoal: 'REACH' | 'IMPRESSIONS' | 'LINK_CLICKS' | 'CONVERSIONS' | 'VALUE';
}
interface FacebookAd {
adId: string;
adSetId: string;
name: string;
status: 'DRAFT' | 'ACTIVE' | 'PAUSED' | 'REJECTED';
creative: {
type: 'IMAGE' | 'VIDEO' | 'CAROUSEL' | 'COLLECTION';
mediaUrl: string;
thumbnailUrl?: string;
headline: string;
description?: string;
callToAction: string;
landingPageUrl: string;
linkCaption?: string;
};
}
interface FacebookAdPerformance {
adId: string;
dateRange: {
start: string;
end: string;
};
metrics: {
impressions: number;
reach: number;
frequency: number;
clicks: number;
uniqueClicks: number;
ctr: number;
spend: number;
cpc: number;
cpm: number;
conversions: number;
conversionRate: number;
costPerConversion: number;
roas: number;
purchaseValue: number;
};
}
export class FacebookAdsConnector {
platformCode = 'FACEBOOK_ADS';
capabilities = {
hasApi: true,
supportsCampaignCreate: true,
supportsAdCreate: true,
supportsPerformanceReport: true,
supportsAudienceTargeting: true,
supportsLookalikeAudiences: true
};
private apiBaseUrl = 'https://graph.facebook.com/v18.0';
private appId = process.env.FACEBOOK_APP_ID || '';
private appSecret = process.env.FACEBOOK_APP_SECRET || '';
private accessToken = process.env.FACEBOOK_ACCESS_TOKEN || '';
async authorize(adAccountId: string): Promise<boolean> {
logger.info(`[FacebookAdsConnector] Authorizing ad account: ${adAccountId}`);
if (!this.appId || this.appId === '') {
logger.warn('[FacebookAdsConnector] No API credentials configured, using mock mode');
return true;
}
try {
const response = await axios.get(`${this.apiBaseUrl}/${adAccountId}`, {
params: {
fields: 'id,name,account_status',
access_token: this.accessToken
}
});
return response.data?.account_status === 1;
} catch (error: any) {
logger.error(`[FacebookAdsConnector] Authorization failed: ${error.message}`);
return false;
}
}
async createCampaign(adAccountId: string, campaign: Partial<FacebookAdCampaign>): Promise<FacebookAdCampaign> {
logger.info(`[FacebookAdsConnector] Creating campaign: ${campaign.name}`);
if (!this.appId || this.appId === '') {
return this.getMockCampaign(campaign);
}
try {
const objectiveMap: Record<string, string> = {
'AWARENESS': 'OUTCOME_AWARENESS',
'TRAFFIC': 'OUTCOME_TRAFFIC',
'ENGAGEMENT': 'OUTCOME_ENGAGEMENT',
'LEADS': 'OUTCOME_LEADS',
'APP_PROMOTION': 'OUTCOME_APP_PROMOTION',
'SALES': 'OUTCOME_SALES'
};
const response = await axios.post(
`${this.apiBaseUrl}/${adAccountId}/campaigns`,
{
name: campaign.name,
objective: objectiveMap[campaign.objective || 'SALES'],
status: 'PAUSED',
special_ad_categories: campaign.specialCategories || [],
daily_budget: campaign.budget?.daily ? Math.round(campaign.budget.daily * 100) : undefined,
lifetime_budget: campaign.budget?.lifetime ? Math.round(campaign.budget.lifetime * 100) : undefined
},
{ params: { access_token: this.accessToken } }
);
return {
campaignId: response.data?.id,
name: campaign.name || '',
objective: campaign.objective || 'SALES',
status: 'DRAFT',
budget: campaign.budget || { currency: 'USD' },
specialCategories: campaign.specialCategories
};
} catch (error: any) {
logger.error(`[FacebookAdsConnector] Create campaign failed: ${error.message}`);
throw error;
}
}
async createAdSet(adAccountId: string, adSet: Partial<FacebookAdSet>): Promise<FacebookAdSet> {
logger.info(`[FacebookAdsConnector] Creating ad set: ${adSet.name}`);
if (!this.appId || this.appId === '') {
return this.getMockAdSet(adSet);
}
try {
const targeting: any = {};
if (adSet.targeting?.ageMin || adSet.targeting?.ageMax) {
targeting.age_min = adSet.targeting.ageMin || 18;
targeting.age_max = adSet.targeting.ageMax || 65;
}
if (adSet.targeting?.genders) {
targeting.genders = adSet.targeting.genders.map(g => g === 'male' ? 1 : 2);
}
if (adSet.targeting?.locations?.countries) {
targeting.geo_locations = { countries: adSet.targeting.locations.countries };
}
if (adSet.targeting?.interests) {
targeting.interests = adSet.targeting.interests.map(id => ({ id, name: id }));
}
const response = await axios.post(
`${this.apiBaseUrl}/${adAccountId}/adsets`,
{
name: adSet.name,
campaign_id: adSet.campaignId,
status: 'PAUSED',
targeting,
daily_budget: adSet.budget?.daily ? Math.round(adSet.budget.daily * 100) : undefined,
lifetime_budget: adSet.budget?.lifetime ? Math.round(adSet.budget.lifetime * 100) : undefined,
bid_strategy: adSet.bidStrategy || 'LOWEST_COST_WITHOUT_CAP',
optimization_goal: adSet.optimizationGoal || 'CONVERSIONS',
billing_event: 'IMPRESSIONS'
},
{ params: { access_token: this.accessToken } }
);
return {
adSetId: response.data?.id,
campaignId: adSet.campaignId || '',
name: adSet.name || '',
status: 'DRAFT',
targeting: adSet.targeting || {},
budget: adSet.budget || { currency: 'USD' },
bidStrategy: adSet.bidStrategy || 'LOWEST_COST_WITHOUT_CAP',
optimizationGoal: adSet.optimizationGoal || 'CONVERSIONS'
};
} catch (error: any) {
logger.error(`[FacebookAdsConnector] Create ad set failed: ${error.message}`);
throw error;
}
}
async createAd(adAccountId: string, ad: Partial<FacebookAd>): Promise<FacebookAd> {
logger.info(`[FacebookAdsConnector] Creating ad: ${ad.name}`);
if (!this.appId || this.appId === '') {
return this.getMockAd(ad);
}
try {
const creativeData: any = {
name: ad.name,
object_story_spec: {
link_data: {
link: ad.creative?.landingPageUrl,
message: ad.creative?.description || '',
name: ad.creative?.headline,
call_to_action: { type: ad.creative?.callToAction || 'SHOP_NOW' },
image_hash: ad.creative?.mediaUrl
},
page_id: adAccountId
}
};
const creativeResponse = await axios.post(
`${this.apiBaseUrl}/${adAccountId}/adcreatives`,
creativeData,
{ params: { access_token: this.accessToken } }
);
const response = await axios.post(
`${this.apiBaseUrl}/${adAccountId}/ads`,
{
name: ad.name,
adset_id: ad.adSetId,
creative: { creative_id: creativeResponse.data?.id },
status: 'PAUSED'
},
{ params: { access_token: this.accessToken } }
);
return {
adId: response.data?.id,
adSetId: ad.adSetId || '',
name: ad.name || '',
status: 'DRAFT',
creative: ad.creative || {
type: 'IMAGE',
mediaUrl: '',
headline: '',
callToAction: 'SHOP_NOW',
landingPageUrl: ''
}
};
} catch (error: any) {
logger.error(`[FacebookAdsConnector] Create ad failed: ${error.message}`);
throw error;
}
}
async getPerformance(adAccountId: string, params: {
adIds?: string[];
campaignIds?: string[];
startDate: string;
endDate: string;
}): Promise<FacebookAdPerformance[]> {
logger.info(`[FacebookAdsConnector] Getting performance for ad account: ${adAccountId}`);
if (!this.appId || this.appId === '') {
return this.getMockPerformance(params);
}
try {
const insightsParams: any = {
level: 'ad',
date_range: {
since: params.startDate,
until: params.endDate
},
fields: 'ad_id,impressions,reach,frequency,clicks,unique_clicks,spend,cpc,cpm,conversions,purchase_roas,action_values',
access_token: this.accessToken
};
if (params.adIds?.length) {
insightsParams.filtering = [{ field: 'ad.id', operator: 'IN', value: params.adIds }];
}
const response = await axios.get(
`${this.apiBaseUrl}/${adAccountId}/insights`,
{ params: insightsParams }
);
return (response.data?.data || []).map((item: any) => ({
adId: item.ad_id,
dateRange: { start: params.startDate, end: params.endDate },
metrics: {
impressions: parseInt(item.impressions) || 0,
reach: parseInt(item.reach) || 0,
frequency: parseFloat(item.frequency) || 0,
clicks: parseInt(item.clicks) || 0,
uniqueClicks: parseInt(item.unique_clicks) || 0,
ctr: parseFloat(item.ctr) || 0,
spend: parseFloat(item.spend) || 0,
cpc: parseFloat(item.cpc) || 0,
cpm: parseFloat(item.cpm) || 0,
conversions: parseInt(item.conversions) || 0,
conversionRate: item.clicks ? (parseInt(item.conversions) / parseInt(item.clicks)) * 100 : 0,
costPerConversion: item.conversions ? parseFloat(item.spend) / parseInt(item.conversions) : 0,
roas: parseFloat(item.purchase_roas?.[0]?.value) || 0,
purchaseValue: parseFloat(item.action_values?.find((a: any) => a.action_type === 'omni_purchase')?.value) || 0
}
}));
} catch (error: any) {
logger.error(`[FacebookAdsConnector] Get performance failed: ${error.message}`);
return this.getMockPerformance(params);
}
}
async updateCampaignStatus(campaignId: string, status: 'ACTIVE' | 'PAUSED'): Promise<boolean> {
logger.info(`[FacebookAdsConnector] Updating campaign ${campaignId} status to ${status}`);
if (!this.appId || this.appId === '') {
return true;
}
try {
await axios.post(
`${this.apiBaseUrl}/${campaignId}`,
{ status: status === 'ACTIVE' ? 'ACTIVE' : 'PAUSED' },
{ params: { access_token: this.accessToken } }
);
return true;
} catch (error: any) {
logger.error(`[FacebookAdsConnector] Update campaign status failed: ${error.message}`);
return false;
}
}
async getCampaigns(adAccountId: string): Promise<FacebookAdCampaign[]> {
logger.info(`[FacebookAdsConnector] Getting campaigns for ad account: ${adAccountId}`);
if (!this.appId || this.appId === '') {
return [
{
campaignId: 'FB_CAMP_001',
name: 'Holiday Sales Campaign',
objective: 'SALES',
status: 'ACTIVE',
budget: { daily: 100, currency: 'USD' }
},
{
campaignId: 'FB_CAMP_002',
name: 'Brand Awareness Q1',
objective: 'AWARENESS',
status: 'PAUSED',
budget: { lifetime: 5000, currency: 'USD' }
}
];
}
try {
const response = await axios.get(`${this.apiBaseUrl}/${adAccountId}/campaigns`, {
params: {
fields: 'id,name,objective,status,daily_budget,lifetime_budget',
access_token: this.accessToken
}
});
return (response.data?.data || []).map((item: any) => ({
campaignId: item.id,
name: item.name,
objective: item.objective?.replace('OUTCOME_', '') as any,
status: item.status,
budget: {
daily: item.daily_budget ? parseInt(item.daily_budget) / 100 : undefined,
lifetime: item.lifetime_budget ? parseInt(item.lifetime_budget) / 100 : undefined,
currency: 'USD'
}
}));
} catch (error: any) {
logger.error(`[FacebookAdsConnector] Get campaigns failed: ${error.message}`);
return [];
}
}
async createLookalikeAudience(adAccountId: string, params: {
name: string;
sourceAudienceId: string;
country: string;
ratio: number;
}): Promise<{ audienceId: string; name: string }> {
logger.info(`[FacebookAdsConnector] Creating lookalike audience: ${params.name}`);
if (!this.appId || this.appId === '') {
return {
audienceId: `FB_LAL_${Date.now()}`,
name: params.name
};
}
try {
const response = await axios.post(
`${this.apiBaseUrl}/${adAccountId}/customaudiences`,
{
name: params.name,
subtype: 'LOOKALIKE',
lookalike_spec: JSON.stringify({
origin_audience_id: params.sourceAudienceId,
country: params.country,
starting_ratio: params.ratio * 0.01,
ratio: params.ratio * 0.01 + 0.01
})
},
{ params: { access_token: this.accessToken } }
);
return {
audienceId: response.data?.id,
name: params.name
};
} catch (error: any) {
logger.error(`[FacebookAdsConnector] Create lookalike audience failed: ${error.message}`);
throw error;
}
}
private getMockCampaign(campaign: Partial<FacebookAdCampaign>): FacebookAdCampaign {
return {
campaignId: `FB_CAMP_${Date.now()}`,
name: campaign.name || 'New Campaign',
objective: campaign.objective || 'SALES',
status: 'DRAFT',
budget: campaign.budget || { daily: 50, currency: 'USD' },
specialCategories: campaign.specialCategories
};
}
private getMockAdSet(adSet: Partial<FacebookAdSet>): FacebookAdSet {
return {
adSetId: `FB_AS_${Date.now()}`,
campaignId: adSet.campaignId || '',
name: adSet.name || 'New Ad Set',
status: 'DRAFT',
targeting: adSet.targeting || { ageMin: 18, ageMax: 65, locations: { countries: ['US'] } },
budget: adSet.budget || { daily: 20, currency: 'USD' },
bidStrategy: adSet.bidStrategy || 'LOWEST_COST_WITHOUT_CAP',
optimizationGoal: adSet.optimizationGoal || 'CONVERSIONS'
};
}
private getMockAd(ad: Partial<FacebookAd>): FacebookAd {
return {
adId: `FB_AD_${Date.now()}`,
adSetId: ad.adSetId || '',
name: ad.name || 'New Ad',
status: 'DRAFT',
creative: ad.creative || {
type: 'IMAGE',
mediaUrl: 'https://example.com/image.jpg',
headline: 'Shop Now',
callToAction: 'SHOP_NOW',
landingPageUrl: 'https://example.com/shop'
}
};
}
private getMockPerformance(params: { startDate: string; endDate: string; adIds?: string[] }): FacebookAdPerformance[] {
const adIds = params.adIds || ['FB_AD_001', 'FB_AD_002'];
return adIds.map(adId => ({
adId,
dateRange: { start: params.startDate, end: params.endDate },
metrics: {
impressions: Math.floor(Math.random() * 150000) + 20000,
reach: Math.floor(Math.random() * 100000) + 15000,
frequency: Math.random() * 3 + 1,
clicks: Math.floor(Math.random() * 8000) + 800,
uniqueClicks: Math.floor(Math.random() * 6000) + 600,
ctr: Math.random() * 4 + 0.5,
spend: Math.random() * 800 + 100,
cpc: Math.random() * 1.5 + 0.3,
cpm: Math.random() * 15 + 3,
conversions: Math.floor(Math.random() * 150) + 15,
conversionRate: Math.random() * 4 + 0.5,
costPerConversion: Math.random() * 25 + 5,
roas: Math.random() * 6 + 1,
purchaseValue: Math.random() * 3000 + 500
}
}));
}
}
export default FacebookAdsConnector;

View File

@@ -0,0 +1,576 @@
import { logger } from '../../utils/logger';
import axios from 'axios';
interface GoogleAdsCampaign {
campaignId: string;
name: string;
type: 'SEARCH' | 'DISPLAY' | 'SHOPPING' | 'VIDEO' | 'PERFORMANCE_MAX';
status: 'DRAFT' | 'ENABLED' | 'PAUSED' | 'REMOVED';
budget: {
amountMicros: number;
currency: string;
};
networkSettings: {
targetGoogleSearch: boolean;
targetSearchNetwork: boolean;
targetContentNetwork: boolean;
targetPartnerSearchNetwork: boolean;
};
biddingStrategy: 'MANUAL_CPC' | 'MANUAL_CPM' | 'TARGET_SPEND' | 'TARGET_CPA' | 'TARGET_ROAS';
}
interface GoogleAdsAdGroup {
adGroupId: string;
campaignId: string;
name: string;
status: 'DRAFT' | 'ENABLED' | 'PAUSED';
type: 'SEARCH_STANDARD' | 'DISPLAY_STANDARD' | 'SHOPPING_PRODUCT_ADS';
cpcBidMicros?: number;
cpmBidMicros?: number;
}
interface GoogleAdsAd {
adId: string;
adGroupId: string;
type: 'TEXT_AD' | 'IMAGE_AD' | 'VIDEO_AD' | 'SHOPPING_AD' | 'RESPONSIVE_SEARCH_AD';
status: 'DRAFT' | 'ENABLED' | 'PAUSED';
content: {
headline?: string[];
description?: string[];
finalUrl?: string;
displayUrl?: string;
imageUrl?: string;
videoUrl?: string;
};
}
interface GoogleAdsPerformance {
campaignId: string;
adGroupId?: string;
adId?: string;
dateRange: {
start: string;
end: string;
};
metrics: {
impressions: number;
clicks: number;
ctr: number;
averageCpc: number;
averageCpm: number;
costMicros: number;
conversions: number;
conversionRate: number;
costPerConversion: number;
roas: number;
allConversionsValue: number;
};
}
interface GoogleAdsKeyword {
keywordId: string;
adGroupId: string;
text: string;
matchType: 'EXACT' | 'PHRASE' | 'BROAD';
status: 'ENABLED' | 'PAUSED';
cpcBidMicros: number;
qualityScore?: number;
}
export class GoogleAdsConnector {
platformCode = 'GOOGLE_ADS';
capabilities = {
hasApi: true,
supportsCampaignCreate: true,
supportsAdCreate: true,
supportsPerformanceReport: true,
supportsKeywordManagement: true,
supportsShoppingAds: true
};
private apiBaseUrl = 'https://googleads.googleapis.com/v15';
private developerToken = process.env.GOOGLE_ADS_DEVELOPER_TOKEN || '';
private clientId = process.env.GOOGLE_ADS_CLIENT_ID || '';
private clientSecret = process.env.GOOGLE_ADS_CLIENT_SECRET || '';
private refreshToken = process.env.GOOGLE_ADS_REFRESH_TOKEN || '';
private customerId = process.env.GOOGLE_ADS_CUSTOMER_ID || '';
async authorize(customerId: string): Promise<boolean> {
logger.info(`[GoogleAdsConnector] Authorizing customer: ${customerId}`);
if (!this.developerToken || this.developerToken === '') {
logger.warn('[GoogleAdsConnector] No API credentials configured, using mock mode');
return true;
}
try {
const response = await axios.get(
`${this.apiBaseUrl}/customers/${customerId}/googleAds:search`,
{
params: { query: 'SELECT customer.id FROM customer LIMIT 1' },
headers: {
'developer-token': this.developerToken,
'Authorization': `Bearer ${this.refreshToken}`
}
}
);
return response.status === 200;
} catch (error: any) {
logger.error(`[GoogleAdsConnector] Authorization failed: ${error.message}`);
return false;
}
}
async createCampaign(customerId: string, campaign: Partial<GoogleAdsCampaign>): Promise<GoogleAdsCampaign> {
logger.info(`[GoogleAdsConnector] Creating campaign: ${campaign.name}`);
if (!this.developerToken || this.developerToken === '') {
return this.getMockCampaign(campaign);
}
try {
const campaignTypeMap: Record<string, string> = {
'SEARCH': 'SEARCH',
'DISPLAY': 'DISPLAY',
'SHOPPING': 'SHOPPING',
'VIDEO': 'VIDEO',
'PERFORMANCE_MAX': 'PERFORMANCE_MAX'
};
const response = await axios.post(
`${this.apiBaseUrl}/customers/${customerId}/campaigns:mutate`,
{
operations: [{
create: {
name: campaign.name,
advertising_channel_type: campaignTypeMap[campaign.type || 'SEARCH'],
status: 'PAUSED',
campaign_budget: `customers/${customerId}/campaignBudgets/${Date.now()}`,
network_settings: {
target_google_search: campaign.networkSettings?.targetGoogleSearch ?? true,
target_search_network: campaign.networkSettings?.targetSearchNetwork ?? true,
target_content_network: campaign.networkSettings?.targetContentNetwork ?? false
},
bidding_strategy_type: campaign.biddingStrategy || 'MANUAL_CPC'
}
}]
},
{
headers: {
'developer-token': this.developerToken,
'Authorization': `Bearer ${this.refreshToken}`
}
}
);
return {
campaignId: response.data?.results?.[0]?.resourceName?.split('/').pop(),
name: campaign.name || '',
type: campaign.type || 'SEARCH',
status: 'DRAFT',
budget: campaign.budget || { amountMicros: 0, currency: 'USD' },
networkSettings: campaign.networkSettings || {
targetGoogleSearch: true,
targetSearchNetwork: true,
targetContentNetwork: false,
targetPartnerSearchNetwork: false
},
biddingStrategy: campaign.biddingStrategy || 'MANUAL_CPC'
};
} catch (error: any) {
logger.error(`[GoogleAdsConnector] Create campaign failed: ${error.message}`);
throw error;
}
}
async createAdGroup(customerId: string, adGroup: Partial<GoogleAdsAdGroup>): Promise<GoogleAdsAdGroup> {
logger.info(`[GoogleAdsConnector] Creating ad group: ${adGroup.name}`);
if (!this.developerToken || this.developerToken === '') {
return this.getMockAdGroup(adGroup);
}
try {
const response = await axios.post(
`${this.apiBaseUrl}/customers/${customerId}/adGroups:mutate`,
{
operations: [{
create: {
name: adGroup.name,
campaign: `customers/${customerId}/campaigns/${adGroup.campaignId}`,
status: 'PAUSED',
type: adGroup.type || 'SEARCH_STANDARD',
cpc_bid_micros: adGroup.cpcBidMicros || 1000000
}
}]
},
{
headers: {
'developer-token': this.developerToken,
'Authorization': `Bearer ${this.refreshToken}`
}
}
);
return {
adGroupId: response.data?.results?.[0]?.resourceName?.split('/').pop(),
campaignId: adGroup.campaignId || '',
name: adGroup.name || '',
status: 'DRAFT',
type: adGroup.type || 'SEARCH_STANDARD',
cpcBidMicros: adGroup.cpcBidMicros
};
} catch (error: any) {
logger.error(`[GoogleAdsConnector] Create ad group failed: ${error.message}`);
throw error;
}
}
async createAd(customerId: string, ad: Partial<GoogleAdsAd>): Promise<GoogleAdsAd> {
logger.info(`[GoogleAdsConnector] Creating ad: ${ad.type}`);
if (!this.developerToken || this.developerToken === '') {
return this.getMockAd(ad);
}
try {
let adData: any = {};
if (ad.type === 'RESPONSIVE_SEARCH_AD') {
adData = {
responsive_search_ad: {
headlines: ad.content?.headline?.map(text => ({ text })) || [],
descriptions: ad.content?.description?.map(text => ({ text })) || [],
final_urls: ad.content?.finalUrl ? [ad.content.finalUrl] : []
}
};
} else if (ad.type === 'TEXT_AD') {
adData = {
text_ad: {
headline: ad.content?.headline?.[0] || '',
description1: ad.content?.description?.[0] || '',
description2: ad.content?.description?.[1] || '',
final_urls: ad.content?.finalUrl ? [ad.content.finalUrl] : []
}
};
}
const response = await axios.post(
`${this.apiBaseUrl}/customers/${customerId}/ads:mutate`,
{
operations: [{
create: {
ad_group: `customers/${customerId}/adGroups/${ad.adGroupId}`,
status: 'PAUSED',
...adData
}
}]
},
{
headers: {
'developer-token': this.developerToken,
'Authorization': `Bearer ${this.refreshToken}`
}
}
);
return {
adId: response.data?.results?.[0]?.resourceName?.split('/').pop(),
adGroupId: ad.adGroupId || '',
type: ad.type || 'TEXT_AD',
status: 'DRAFT',
content: ad.content || {}
};
} catch (error: any) {
logger.error(`[GoogleAdsConnector] Create ad failed: ${error.message}`);
throw error;
}
}
async addKeywords(customerId: string, adGroupId: string, keywords: Array<{
text: string;
matchType: 'EXACT' | 'PHRASE' | 'BROAD';
cpcBidMicros: number;
}>): Promise<GoogleAdsKeyword[]> {
logger.info(`[GoogleAdsConnector] Adding ${keywords.length} keywords to ad group: ${adGroupId}`);
if (!this.developerToken || this.developerToken === '') {
return keywords.map((kw, i) => ({
keywordId: `KW_${Date.now()}_${i}`,
adGroupId,
text: kw.text,
matchType: kw.matchType,
status: 'ENABLED',
cpcBidMicros: kw.cpcBidMicros
}));
}
try {
const response = await axios.post(
`${this.apiBaseUrl}/customers/${customerId}/adGroupCriteria:mutate`,
{
operations: keywords.map(kw => ({
create: {
ad_group: `customers/${customerId}/adGroups/${adGroupId}`,
keyword: {
text: kw.text,
match_type: kw.matchType
},
cpc_bid_micros: kw.cpcBidMicros
}
}))
},
{
headers: {
'developer-token': this.developerToken,
'Authorization': `Bearer ${this.refreshToken}`
}
}
);
return (response.data?.results || []).map((result: any, i: number) => ({
keywordId: result.resourceName?.split('/').pop(),
adGroupId,
text: keywords[i].text,
matchType: keywords[i].matchType,
status: 'ENABLED',
cpcBidMicros: keywords[i].cpcBidMicros
}));
} catch (error: any) {
logger.error(`[GoogleAdsConnector] Add keywords failed: ${error.message}`);
throw error;
}
}
async getPerformance(customerId: string, params: {
campaignIds?: string[];
adGroupIds?: string[];
adIds?: string[];
startDate: string;
endDate: string;
}): Promise<GoogleAdsPerformance[]> {
logger.info(`[GoogleAdsConnector] Getting performance for customer: ${customerId}`);
if (!this.developerToken || this.developerToken === '') {
return this.getMockPerformance(params);
}
try {
const metrics = 'metrics.impressions,metrics.clicks,metrics.ctr,metrics.average_cpc,metrics.average_cpm,metrics.cost_micros,metrics.conversions,metrics.conversions_from_interactions_rate,metrics.cost_per_conversion,metrics.all_conversions_value';
const segments = 'segments.date';
let query = `SELECT campaign.id, ad_group.id, ad_group_ad.ad.id, ${metrics} FROM ad_group_ad WHERE segments.date BETWEEN '${params.startDate}' AND '${params.endDate}'`;
if (params.campaignIds?.length) {
query += ` AND campaign.id IN (${params.campaignIds.map(id => `'${id}'`).join(',')})`;
}
const response = await axios.post(
`${this.apiBaseUrl}/customers/${customerId}/googleAds:search`,
{ query },
{
headers: {
'developer-token': this.developerToken,
'Authorization': `Bearer ${this.refreshToken}`
}
}
);
return (response.data?.results || []).map((item: any) => ({
campaignId: item.campaign?.id,
adGroupId: item.adGroup?.id,
adId: item.adGroupAd?.ad?.id,
dateRange: { start: params.startDate, end: params.endDate },
metrics: {
impressions: parseInt(item.metrics?.impressions) || 0,
clicks: parseInt(item.metrics?.clicks) || 0,
ctr: parseFloat(item.metrics?.ctr) || 0,
averageCpc: parseInt(item.metrics?.averageCpc) || 0,
averageCpm: parseInt(item.metrics?.averageCpm) || 0,
costMicros: parseInt(item.metrics?.costMicros) || 0,
conversions: parseFloat(item.metrics?.conversions) || 0,
conversionRate: parseFloat(item.metrics?.conversionsFromInteractionsRate) || 0,
costPerConversion: parseInt(item.metrics?.costPerConversion) || 0,
roas: item.metrics?.costPerConversion ?
(parseFloat(item.metrics?.allConversionsValue) / (parseInt(item.metrics?.costMicros) / 1000000)) : 0,
allConversionsValue: parseFloat(item.metrics?.allConversionsValue) || 0
}
}));
} catch (error: any) {
logger.error(`[GoogleAdsConnector] Get performance failed: ${error.message}`);
return this.getMockPerformance(params);
}
}
async updateCampaignStatus(campaignId: string, status: 'ENABLED' | 'PAUSED'): Promise<boolean> {
logger.info(`[GoogleAdsConnector] Updating campaign ${campaignId} status to ${status}`);
if (!this.developerToken || this.developerToken === '') {
return true;
}
try {
await axios.post(
`${this.apiBaseUrl}/customers/${this.customerId}/campaigns:mutate`,
{
operations: [{
update: {
resource_name: `customers/${this.customerId}/campaigns/${campaignId}`,
status
},
update_mask: 'status'
}]
},
{
headers: {
'developer-token': this.developerToken,
'Authorization': `Bearer ${this.refreshToken}`
}
}
);
return true;
} catch (error: any) {
logger.error(`[GoogleAdsConnector] Update campaign status failed: ${error.message}`);
return false;
}
}
async getCampaigns(customerId: string): Promise<GoogleAdsCampaign[]> {
logger.info(`[GoogleAdsConnector] Getting campaigns for customer: ${customerId}`);
if (!this.developerToken || this.developerToken === '') {
return [
{
campaignId: 'GOOG_CAMP_001',
name: 'Search Campaign - Electronics',
type: 'SEARCH',
status: 'ENABLED',
budget: { amountMicros: 50000000, currency: 'USD' },
networkSettings: {
targetGoogleSearch: true,
targetSearchNetwork: true,
targetContentNetwork: false,
targetPartnerSearchNetwork: false
},
biddingStrategy: 'TARGET_SPEND'
},
{
campaignId: 'GOOG_CAMP_002',
name: 'Shopping Campaign - Products',
type: 'SHOPPING',
status: 'ENABLED',
budget: { amountMicros: 100000000, currency: 'USD' },
networkSettings: {
targetGoogleSearch: true,
targetSearchNetwork: false,
targetContentNetwork: false,
targetPartnerSearchNetwork: false
},
biddingStrategy: 'TARGET_ROAS'
}
];
}
try {
const query = `SELECT campaign.id, campaign.name, campaign.advertising_channel_type, campaign.status, campaign_budget.amount_micros FROM campaign WHERE campaign.status != 'REMOVED'`;
const response = await axios.post(
`${this.apiBaseUrl}/customers/${customerId}/googleAds:search`,
{ query },
{
headers: {
'developer-token': this.developerToken,
'Authorization': `Bearer ${this.refreshToken}`
}
}
);
return (response.data?.results || []).map((item: any) => ({
campaignId: item.campaign?.id,
name: item.campaign?.name,
type: item.campaign?.advertisingChannelType as any,
status: item.campaign?.status as any,
budget: {
amountMicros: parseInt(item.campaignBudget?.amountMicros) || 0,
currency: 'USD'
},
networkSettings: {
targetGoogleSearch: true,
targetSearchNetwork: true,
targetContentNetwork: false,
targetPartnerSearchNetwork: false
},
biddingStrategy: 'MANUAL_CPC'
}));
} catch (error: any) {
logger.error(`[GoogleAdsConnector] Get campaigns failed: ${error.message}`);
return [];
}
}
private getMockCampaign(campaign: Partial<GoogleAdsCampaign>): GoogleAdsCampaign {
return {
campaignId: `GOOG_CAMP_${Date.now()}`,
name: campaign.name || 'New Campaign',
type: campaign.type || 'SEARCH',
status: 'DRAFT',
budget: campaign.budget || { amountMicros: 50000000, currency: 'USD' },
networkSettings: campaign.networkSettings || {
targetGoogleSearch: true,
targetSearchNetwork: true,
targetContentNetwork: false,
targetPartnerSearchNetwork: false
},
biddingStrategy: campaign.biddingStrategy || 'MANUAL_CPC'
};
}
private getMockAdGroup(adGroup: Partial<GoogleAdsAdGroup>): GoogleAdsAdGroup {
return {
adGroupId: `GOOG_AG_${Date.now()}`,
campaignId: adGroup.campaignId || '',
name: adGroup.name || 'New Ad Group',
status: 'DRAFT',
type: adGroup.type || 'SEARCH_STANDARD',
cpcBidMicros: adGroup.cpcBidMicros || 1000000
};
}
private getMockAd(ad: Partial<GoogleAdsAd>): GoogleAdsAd {
return {
adId: `GOOG_AD_${Date.now()}`,
adGroupId: ad.adGroupId || '',
type: ad.type || 'RESPONSIVE_SEARCH_AD',
status: 'DRAFT',
content: ad.content || {
headline: ['Shop Now', 'Best Deals'],
description: ['Find the best products here.', 'Free shipping on all orders.'],
finalUrl: 'https://example.com/shop'
}
};
}
private getMockPerformance(params: { startDate: string; endDate: string; campaignIds?: string[] }): GoogleAdsPerformance[] {
const campaignIds = params.campaignIds || ['GOOG_CAMP_001', 'GOOG_CAMP_002'];
return campaignIds.map(campaignId => ({
campaignId,
dateRange: { start: params.startDate, end: params.endDate },
metrics: {
impressions: Math.floor(Math.random() * 200000) + 30000,
clicks: Math.floor(Math.random() * 10000) + 1000,
ctr: Math.random() * 5 + 1,
averageCpc: Math.floor(Math.random() * 2000000) + 500000,
averageCpm: Math.floor(Math.random() * 10000000) + 2000000,
costMicros: Math.floor(Math.random() * 1000000000) + 100000000,
conversions: Math.floor(Math.random() * 200) + 20,
conversionRate: Math.random() * 5 + 0.5,
costPerConversion: Math.floor(Math.random() * 50000000) + 10000000,
roas: Math.random() * 8 + 1,
allConversionsValue: Math.random() * 50000 + 5000
}
}));
}
}
export default GoogleAdsConnector;

View File

@@ -0,0 +1,318 @@
import { logger } from '../../utils/logger';
import { IPlatformConnector, PlatformProduct, PublishResult } from './IPlatformConnector';
import axios from 'axios';
interface TemuProduct {
goods_id: string;
goods_name: string;
goods_desc: string;
price: number;
currency: string;
thumb_url: string;
image_urls: string[];
sku_list: Array<{
sku_id: string;
price: number;
stock: number;
spec_values: Record<string, string>;
}>;
cat_id: string;
goods_properties: Record<string, any>;
}
interface TemuOrder {
order_sn: string;
order_status: number;
create_time: number;
buyer_info: {
buyer_id: string;
buyer_name: string;
};
item_list: Array<{
goods_id: string;
sku_id: string;
quantity: number;
price: number;
}>;
shipping_info: {
address_line1: string;
city: string;
state: string;
country: string;
zip_code: string;
};
order_amount: number;
currency: string;
}
export class TemuConnector implements IPlatformConnector {
platformCode = 'TEMU';
capabilities = {
hasApi: true,
supportsPriceSync: true,
supportsInventorySync: true,
supportsOrderPull: true
};
private apiBaseUrl = 'https://api.temu.com';
private appKey = process.env.TEMU_APP_KEY || '';
private appSecret = process.env.TEMU_APP_SECRET || '';
async authorize(shopId: string): Promise<boolean> {
logger.info(`[TemuConnector] Authorizing shop: ${shopId}`);
return true;
}
async pullProducts(shopId: string, params?: { page?: number; pageSize?: number }): Promise<PlatformProduct[]> {
logger.info(`[TemuConnector] Pulling products for shop: ${shopId}`);
if (!this.appKey || this.appKey === '') {
return this.getMockProducts();
}
try {
const response = await axios.post(`${this.apiBaseUrl}/api/goods/search`, {
mall_id: shopId,
page: params?.page || 1,
page_size: params?.pageSize || 50
});
const products: TemuProduct[] = response.data?.result?.goods_list || [];
return products.map(this.mapToPlatformProduct);
} catch (error) {
logger.warn('[TemuConnector] API call failed, returning mock data');
return this.getMockProducts();
}
}
async pullOrders(shopId: string, params?: { startTime?: number; endTime?: number }): Promise<TemuOrder[]> {
logger.info(`[TemuConnector] Pulling orders for shop: ${shopId}`);
if (!this.appKey || this.appKey === '') {
return this.getMockOrders();
}
try {
const response = await axios.post(`${this.apiBaseUrl}/api/order/search`, {
mall_id: shopId,
start_time: params?.startTime || Math.floor(Date.now() / 1000) - 86400 * 7,
end_time: params?.endTime || Math.floor(Date.now() / 1000)
});
return response.data?.result?.order_list || [];
} catch (error) {
logger.warn('[TemuConnector] Order API call failed, returning mock data');
return this.getMockOrders();
}
}
async pushListing(shopId: string, product: PlatformProduct): Promise<PublishResult> {
logger.info(`[TemuConnector] Pushing listing to Temu: ${product.title}`);
if (!this.appKey || this.appKey === '') {
return {
success: true,
externalId: `TEMU-${Date.now()}`,
publishUrl: `https://www.temu.com/product-TEMU-${Date.now()}.html`
};
}
try {
const temuProduct = this.mapFromPlatformProduct(product);
const response = await axios.post(
`${this.apiBaseUrl}/api/goods/create`,
{ mall_id: shopId, goods: temuProduct }
);
return {
success: true,
externalId: response.data?.result?.goods_id,
publishUrl: `https://www.temu.com/product-${response.data?.result?.goods_id}.html`
};
} catch (error: any) {
logger.error(`[TemuConnector] Push listing failed: ${error.message}`);
return {
success: false,
error: error.message
};
}
}
async updatePrice(shopId: string, externalId: string, newPrice: number): Promise<boolean> {
logger.info(`[TemuConnector] Updating price for ${externalId} to ${newPrice}`);
if (!this.appKey || this.appKey === '') {
return true;
}
try {
await axios.post(`${this.apiBaseUrl}/api/goods/update_price`, {
mall_id: shopId,
goods_id: externalId,
price: newPrice * 100
});
return true;
} catch (error: any) {
logger.error(`[TemuConnector] Update price failed: ${error.message}`);
return false;
}
}
async syncInventory(shopId: string, externalId: string, quantity: number): Promise<boolean> {
logger.info(`[TemuConnector] Syncing inventory for ${externalId}: ${quantity}`);
if (!this.appKey || this.appKey === '') {
return true;
}
try {
await axios.post(`${this.apiBaseUrl}/api/goods/update_stock`, {
mall_id: shopId,
goods_id: externalId,
stock: quantity
});
return true;
} catch (error: any) {
logger.error(`[TemuConnector] Sync inventory failed: ${error.message}`);
return false;
}
}
async calculateShipping(shopId: string, params: { countryCode: string; weight: number }): Promise<{
carrier: string;
cost: number;
currency: string;
estimatedDays: number;
}> {
const baseCost = params.countryCode === 'US' ? 0 : 5.99;
const weightCost = Math.max(0, (params.weight - 1) * 1.5);
return {
carrier: 'Temu Express',
cost: baseCost + weightCost,
currency: 'USD',
estimatedDays: params.countryCode === 'US' ? 7 : 15
};
}
private mapToPlatformProduct(temuProduct: TemuProduct): PlatformProduct {
return {
externalId: temuProduct.goods_id,
title: temuProduct.goods_name,
description: temuProduct.goods_desc,
price: temuProduct.price / 100,
currency: temuProduct.currency || 'USD',
images: [temuProduct.thumb_url, ...temuProduct.image_urls],
skus: temuProduct.sku_list.map(sku => ({
skuId: sku.sku_id,
price: sku.price / 100,
stock: sku.stock,
specs: sku.spec_values
})),
platform: 'TEMU',
category: temuProduct.cat_id,
attributes: temuProduct.goods_properties
};
}
private mapFromPlatformProduct(product: PlatformProduct): Partial<TemuProduct> {
return {
goods_name: product.title,
goods_desc: product.description || '',
price: Math.round(product.price * 100),
currency: product.currency || 'USD',
thumb_url: product.images[0] || '',
image_urls: product.images.slice(1),
cat_id: product.category || '',
goods_properties: product.attributes || {}
};
}
private getMockProducts(): PlatformProduct[] {
return [
{
externalId: 'TEMU_PROD_001',
title: 'Temu Budget Wireless Earbuds',
description: 'Affordable wireless earbuds with decent sound quality and 20h battery life. Great value for money!',
price: 12.99,
currency: 'USD',
images: [
'https://example.com/temu/earbuds-1.jpg',
'https://example.com/temu/earbuds-2.jpg'
],
skus: [
{ skuId: 'TEMU_SKU_001_W', price: 12.99, stock: 1000, specs: { color: 'White' } },
{ skuId: 'TEMU_SKU_001_B', price: 12.99, stock: 800, specs: { color: 'Black' } }
],
platform: 'TEMU',
category: 'Electronics',
attributes: { brand: 'TemuBasic', features: 'Bluetooth 5.0' }
},
{
externalId: 'TEMU_PROD_002',
title: 'Kitchen Storage Organizer Set',
description: 'Complete kitchen storage organizer set with 10 pieces. Perfect for organizing your pantry.',
price: 19.99,
currency: 'USD',
images: ['https://example.com/temu/storage-set.jpg'],
skus: [
{ skuId: 'TEMU_SKU_002', price: 19.99, stock: 500, specs: {} }
],
platform: 'TEMU',
category: 'Home & Kitchen'
},
{
externalId: 'TEMU_PROD_003',
title: 'Kids Art Supplies Kit',
description: 'Complete art supplies kit for kids including crayons, markers, colored pencils and more.',
price: 8.99,
currency: 'USD',
images: ['https://example.com/temu/art-kit.jpg'],
skus: [
{ skuId: 'TEMU_SKU_003', price: 8.99, stock: 2000, specs: {} }
],
platform: 'TEMU',
category: 'Toys & Games'
}
];
}
private getMockOrders(): TemuOrder[] {
return [
{
order_sn: 'TEMU_ORDER_001',
order_status: 2,
create_time: Math.floor(Date.now() / 1000) - 7200,
buyer_info: {
buyer_id: 'TEMU_USER_001',
buyer_name: 'temu_shopper'
},
item_list: [
{
goods_id: 'TEMU_PROD_001',
sku_id: 'TEMU_SKU_001_W',
quantity: 2,
price: 1299
},
{
goods_id: 'TEMU_PROD_002',
sku_id: 'TEMU_SKU_002',
quantity: 1,
price: 1999
}
],
shipping_info: {
address_line1: '456 Oak Avenue',
city: 'New York',
state: 'NY',
country: 'US',
zip_code: '10001'
},
order_amount: 4597,
currency: 'USD'
}
];
}
}
export default TemuConnector;

View File

@@ -0,0 +1,403 @@
import { logger } from '../../utils/logger';
import axios from 'axios';
interface TikTokAdCampaign {
campaignId: string;
name: string;
objective: 'AWARENESS' | 'CONSIDERATION' | 'CONVERSIONS';
status: 'DRAFT' | 'ACTIVE' | 'PAUSED' | 'COMPLETED';
budget: {
daily: number;
lifetime?: number;
currency: string;
};
schedule: {
startTime: number;
endTime?: number;
};
}
interface TikTokAdGroup {
adGroupId: string;
campaignId: string;
name: string;
status: 'DRAFT' | 'ACTIVE' | 'PAUSED';
targeting: {
ageMin?: number;
ageMax?: number;
genders?: string[];
locations?: string[];
interests?: string[];
behaviors?: string[];
};
budget: {
daily: number;
currency: string;
};
bidStrategy: 'LOWEST_COST' | 'TARGET_COST' | 'BID_CAP';
bidAmount?: number;
}
interface TikTokAd {
adId: string;
adGroupId: string;
name: string;
status: 'DRAFT' | 'ACTIVE' | 'PAUSED' | 'REJECTED';
creative: {
type: 'VIDEO' | 'IMAGE';
mediaUrl: string;
thumbnailUrl?: string;
headline: string;
description?: string;
callToAction: string;
landingPageUrl: string;
};
}
interface TikTokAdPerformance {
adId: string;
dateRange: {
start: string;
end: string;
};
metrics: {
impressions: number;
clicks: number;
ctr: number;
spend: number;
cpc: number;
cpm: number;
conversions: number;
conversionRate: number;
costPerConversion: number;
roas: number;
};
}
export class TikTokAdsConnector {
platformCode = 'TIKTOK_ADS';
capabilities = {
hasApi: true,
supportsCampaignCreate: true,
supportsAdCreate: true,
supportsPerformanceReport: true,
supportsAutoOptimization: true
};
private apiBaseUrl = 'https://business-api.tiktok.com/open_api/v1.3';
private appId = process.env.TIKTOK_ADS_APP_ID || '';
private appSecret = process.env.TIKTOK_ADS_APP_SECRET || '';
private accessToken = process.env.TIKTOK_ADS_ACCESS_TOKEN || '';
async authorize(advertiserId: string): Promise<boolean> {
logger.info(`[TikTokAdsConnector] Authorizing advertiser: ${advertiserId}`);
if (!this.appId || this.appId === '') {
logger.warn('[TikTokAdsConnector] No API credentials configured, using mock mode');
return true;
}
try {
const response = await axios.get(`${this.apiBaseUrl}/advertiser/info/`, {
params: { advertiser_id: advertiserId },
headers: { 'Access-Token': this.accessToken }
});
return response.data?.code === 0;
} catch (error: any) {
logger.error(`[TikTokAdsConnector] Authorization failed: ${error.message}`);
return false;
}
}
async createCampaign(advertiserId: string, campaign: Partial<TikTokAdCampaign>): Promise<TikTokAdCampaign> {
logger.info(`[TikTokAdsConnector] Creating campaign: ${campaign.name}`);
if (!this.appId || this.appId === '') {
return this.getMockCampaign(campaign);
}
try {
const response = await axios.post(
`${this.apiBaseUrl}/campaign/create/`,
{
advertiser_id: advertiserId,
campaign_name: campaign.name,
objective_type: campaign.objective || 'CONVERSIONS',
budget_mode: campaign.budget?.lifetime ? 'BUDGET_MODE_TOTAL' : 'BUDGET_MODE_DAY',
budget: campaign.budget?.lifetime || campaign.budget?.daily || 0,
operation_status: 'ENABLE'
},
{ headers: { 'Access-Token': this.accessToken } }
);
return {
campaignId: response.data?.data?.campaign_id,
name: campaign.name || '',
objective: campaign.objective || 'CONVERSIONS',
status: 'DRAFT',
budget: campaign.budget || { daily: 0, currency: 'USD' },
schedule: campaign.schedule || { startTime: Date.now() }
};
} catch (error: any) {
logger.error(`[TikTokAdsConnector] Create campaign failed: ${error.message}`);
throw error;
}
}
async createAdGroup(advertiserId: string, adGroup: Partial<TikTokAdGroup>): Promise<TikTokAdGroup> {
logger.info(`[TikTokAdsConnector] Creating ad group: ${adGroup.name}`);
if (!this.appId || this.appId === '') {
return this.getMockAdGroup(adGroup);
}
try {
const response = await axios.post(
`${this.apiBaseUrl}/adgroup/create/`,
{
advertiser_id: advertiserId,
campaign_id: adGroup.campaignId,
adgroup_name: adGroup.name,
budget_mode: 'BUDGET_MODE_DAY',
budget: adGroup.budget?.daily || 20,
bid_strategy: adGroup.bidStrategy || 'LOWEST_COST',
target_audience: {
age_range: adGroup.targeting?.ageMin ? [
{ min: adGroup.targeting.ageMin, max: adGroup.targeting.ageMax || 55 }
] : undefined,
gender: adGroup.targeting?.genders,
location_ids: adGroup.targeting?.locations
}
},
{ headers: { 'Access-Token': this.accessToken } }
);
return {
adGroupId: response.data?.data?.adgroup_id,
campaignId: adGroup.campaignId || '',
name: adGroup.name || '',
status: 'DRAFT',
targeting: adGroup.targeting || {},
budget: adGroup.budget || { daily: 20, currency: 'USD' },
bidStrategy: adGroup.bidStrategy || 'LOWEST_COST'
};
} catch (error: any) {
logger.error(`[TikTokAdsConnector] Create ad group failed: ${error.message}`);
throw error;
}
}
async createAd(advertiserId: string, ad: Partial<TikTokAd>): Promise<TikTokAd> {
logger.info(`[TikTokAdsConnector] Creating ad: ${ad.name}`);
if (!this.appId || this.appId === '') {
return this.getMockAd(ad);
}
try {
const response = await axios.post(
`${this.apiBaseUrl}/ad/create/`,
{
advertiser_id: advertiserId,
adgroup_id: ad.adGroupId,
creatives: [{
creative_type: ad.creative?.type || 'VIDEO',
video_url: ad.creative?.mediaUrl,
title: ad.creative?.headline,
description: ad.creative?.description,
call_to_action: ad.creative?.callToAction,
landing_page_url: ad.creative?.landingPageUrl
}]
},
{ headers: { 'Access-Token': this.accessToken } }
);
return {
adId: response.data?.data?.ad_id,
adGroupId: ad.adGroupId || '',
name: ad.name || '',
status: 'DRAFT',
creative: ad.creative || {
type: 'VIDEO',
mediaUrl: '',
headline: '',
callToAction: 'SHOP_NOW',
landingPageUrl: ''
}
};
} catch (error: any) {
logger.error(`[TikTokAdsConnector] Create ad failed: ${error.message}`);
throw error;
}
}
async getPerformance(advertiserId: string, params: {
adIds?: string[];
campaignIds?: string[];
startDate: string;
endDate: string;
}): Promise<TikTokAdPerformance[]> {
logger.info(`[TikTokAdsConnector] Getting performance for advertiser: ${advertiserId}`);
if (!this.appId || this.appId === '') {
return this.getMockPerformance(params);
}
try {
const response = await axios.get(`${this.apiBaseUrl}/report/integrated/get/`, {
params: {
advertiser_id: advertiserId,
report_type: 'BASIC',
data_level: 'AUCTION_AD',
dimensions: ['ad_id'],
metrics: ['impressions', 'clicks', 'spend', 'conversions', 'conversion_rate'],
start_date: params.startDate,
end_date: params.endDate,
filtering: params.adIds ? [{ field: 'ad_ids', operator: 'IN', value: params.adIds }] : undefined
},
headers: { 'Access-Token': this.accessToken }
});
return (response.data?.data?.list || []).map((item: any) => ({
adId: item.dimensions.ad_id,
dateRange: { start: params.startDate, end: params.endDate },
metrics: {
impressions: item.metrics.impressions || 0,
clicks: item.metrics.clicks || 0,
ctr: item.metrics.click_rate || 0,
spend: item.metrics.spend || 0,
cpc: item.metrics.cost_per_click || 0,
cpm: item.metrics.cost_per_mille || 0,
conversions: item.metrics.conversions || 0,
conversionRate: item.metrics.conversion_rate || 0,
costPerConversion: item.metrics.cost_per_conversion || 0,
roas: item.metrics.roas || 0
}
}));
} catch (error: any) {
logger.error(`[TikTokAdsConnector] Get performance failed: ${error.message}`);
return this.getMockPerformance(params);
}
}
async updateCampaignStatus(campaignId: string, status: 'ACTIVE' | 'PAUSED'): Promise<boolean> {
logger.info(`[TikTokAdsConnector] Updating campaign ${campaignId} status to ${status}`);
if (!this.appId || this.appId === '') {
return true;
}
try {
await axios.post(
`${this.apiBaseUrl}/campaign/status/update/`,
{
campaign_id: campaignId,
operation_status: status === 'ACTIVE' ? 'ENABLE' : 'DISABLE'
},
{ headers: { 'Access-Token': this.accessToken } }
);
return true;
} catch (error: any) {
logger.error(`[TikTokAdsConnector] Update campaign status failed: ${error.message}`);
return false;
}
}
async getCampaigns(advertiserId: string): Promise<TikTokAdCampaign[]> {
logger.info(`[TikTokAdsConnector] Getting campaigns for advertiser: ${advertiserId}`);
if (!this.appId || this.appId === '') {
return [
{
campaignId: 'TK_CAMP_001',
name: 'Summer Sale Campaign',
objective: 'CONVERSIONS',
status: 'ACTIVE',
budget: { daily: 100, currency: 'USD' },
schedule: { startTime: Date.now() - 86400 * 7 }
},
{
campaignId: 'TK_CAMP_002',
name: 'Brand Awareness Q1',
objective: 'AWARENESS',
status: 'PAUSED',
budget: { daily: 50, currency: 'USD' },
schedule: { startTime: Date.now() - 86400 * 30 }
}
];
}
try {
const response = await axios.get(`${this.apiBaseUrl}/campaign/get/`, {
params: { advertiser_id: advertiserId },
headers: { 'Access-Token': this.accessToken }
});
return response.data?.data?.list || [];
} catch (error: any) {
logger.error(`[TikTokAdsConnector] Get campaigns failed: ${error.message}`);
return [];
}
}
private getMockCampaign(campaign: Partial<TikTokAdCampaign>): TikTokAdCampaign {
return {
campaignId: `TK_CAMP_${Date.now()}`,
name: campaign.name || 'New Campaign',
objective: campaign.objective || 'CONVERSIONS',
status: 'DRAFT',
budget: campaign.budget || { daily: 50, currency: 'USD' },
schedule: campaign.schedule || { startTime: Date.now() }
};
}
private getMockAdGroup(adGroup: Partial<TikTokAdGroup>): TikTokAdGroup {
return {
adGroupId: `TK_AG_${Date.now()}`,
campaignId: adGroup.campaignId || '',
name: adGroup.name || 'New Ad Group',
status: 'DRAFT',
targeting: adGroup.targeting || { ageMin: 18, ageMax: 44, locations: ['US'] },
budget: adGroup.budget || { daily: 20, currency: 'USD' },
bidStrategy: adGroup.bidStrategy || 'LOWEST_COST'
};
}
private getMockAd(ad: Partial<TikTokAd>): TikTokAd {
return {
adId: `TK_AD_${Date.now()}`,
adGroupId: ad.adGroupId || '',
name: ad.name || 'New Ad',
status: 'DRAFT',
creative: ad.creative || {
type: 'VIDEO',
mediaUrl: 'https://example.com/video.mp4',
headline: 'Shop Now',
callToAction: 'SHOP_NOW',
landingPageUrl: 'https://example.com/shop'
}
};
}
private getMockPerformance(params: { startDate: string; endDate: string; adIds?: string[] }): TikTokAdPerformance[] {
const adIds = params.adIds || ['TK_AD_001', 'TK_AD_002'];
return adIds.map(adId => ({
adId,
dateRange: { start: params.startDate, end: params.endDate },
metrics: {
impressions: Math.floor(Math.random() * 100000) + 10000,
clicks: Math.floor(Math.random() * 5000) + 500,
ctr: Math.random() * 5 + 1,
spend: Math.random() * 500 + 50,
cpc: Math.random() * 2 + 0.5,
cpm: Math.random() * 10 + 2,
conversions: Math.floor(Math.random() * 100) + 10,
conversionRate: Math.random() * 5 + 1,
costPerConversion: Math.random() * 20 + 5,
roas: Math.random() * 5 + 1
}
}));
}
}
export default TikTokAdsConnector;

View File

@@ -1,63 +1,304 @@
import { IPlatformConnector, PlatformProduct, PublishResult } from './IPlatformConnector';
import { logger } from '../../utils/logger';
import { VaultService } from '../../services/VaultService';
import { IPlatformConnector, PlatformProduct, PublishResult } from './IPlatformConnector';
import axios from 'axios';
interface TikTokProduct {
product_id: string;
title: string;
description: string;
price: { original_price: string; currency: string };
main_image: string;
images: string[];
skus: Array<{
id: string;
price: string;
stock: number;
specs: Record<string, string>;
}>;
category_id: string;
attributes: Record<string, any>;
}
interface TikTokOrder {
order_id: string;
order_status: string;
create_time: number;
buyer_info: {
user_id: string;
username: string;
};
items: Array<{
product_id: string;
sku_id: string;
quantity: number;
price: string;
}>;
shipping_info: {
address: string;
city: string;
state: string;
country: string;
zip_code: string;
};
total_amount: string;
currency: string;
}
/**
* [CORE_INT_01] TikTok Shop 平台连接器 (TK Shop API)
* @description 封装 TikTok Shop 开放平台 API支持授权、拉取、发布与同步
*/
export class TikTokConnector implements IPlatformConnector {
platformCode = 'TikTok';
platformCode = 'TIKTOK';
capabilities = {
hasApi: true,
supportsPriceSync: true,
supportsInventorySync: true,
supportsOrderPull: true,
supportsOrderPull: true
};
/**
* 平台授权校验
*/
private apiBaseUrl = 'https://open-api.tiktokglobalshop.com';
private appKey = process.env.TIKTOK_APP_KEY || '';
private appSecret = process.env.TIKTOK_APP_SECRET || '';
async authorize(shopId: string): Promise<boolean> {
logger.info(`[TikTokConnector] Authorizing shop: ${shopId}`);
return true;
}
async pullProducts(shopId: string, params?: { page?: number; pageSize?: number }): Promise<PlatformProduct[]> {
logger.info(`[TikTokConnector] Pulling products for shop: ${shopId}`);
if (!this.appKey || this.appKey === '') {
return this.getMockProducts();
}
try {
// 从保险库获取 Access Token
// const token = await VaultService.getDecryptedCredential({ tenantId, userId, traceId }, id);
logger.info(`[TikTokConnector] Authorizing shop: ${shopId}`);
return true; // Mock implementation
} catch (err) {
const response = await axios.get(`${this.apiBaseUrl}/api/products/search`, {
params: {
shop_id: shopId,
page_number: params?.page || 1,
page_size: params?.pageSize || 50
}
});
const products: TikTokProduct[] = response.data?.data?.products || [];
return products.map(this.mapToPlatformProduct);
} catch (error) {
logger.warn('[TikTokConnector] API call failed, returning mock data');
return this.getMockProducts();
}
}
async pullOrders(shopId: string, params?: { startTime?: number; endTime?: number }): Promise<TikTokOrder[]> {
logger.info(`[TikTokConnector] Pulling orders for shop: ${shopId}`);
if (!this.appKey || this.appKey === '') {
return this.getMockOrders();
}
try {
const response = await axios.get(`${this.apiBaseUrl}/api/orders/search`, {
params: {
shop_id: shopId,
start_time: params?.startTime || Math.floor(Date.now() / 1000) - 86400 * 7,
end_time: params?.endTime || Math.floor(Date.now() / 1000)
}
});
return response.data?.data?.orders || [];
} catch (error) {
logger.warn('[TikTokConnector] Order API call failed, returning mock data');
return this.getMockOrders();
}
}
async pushListing(shopId: string, product: PlatformProduct): Promise<PublishResult> {
logger.info(`[TikTokConnector] Pushing listing to TikTok Shop: ${product.title}`);
if (!this.appKey || this.appKey === '') {
return {
success: true,
externalId: `TK-${Date.now()}`,
publishUrl: `https://shop.tiktok.com/product/TK-${Date.now()}`
};
}
try {
const tiktokProduct = this.mapFromPlatformProduct(product);
const response = await axios.post(
`${this.apiBaseUrl}/api/products/create`,
{ shop_id: shopId, product: tiktokProduct }
);
return {
success: true,
externalId: response.data?.data?.product_id,
publishUrl: `https://shop.tiktok.com/product/${response.data?.data?.product_id}`
};
} catch (error: any) {
logger.error(`[TikTokConnector] Push listing failed: ${error.message}`);
return {
success: false,
error: error.message
};
}
}
async updatePrice(shopId: string, externalId: string, newPrice: number): Promise<boolean> {
logger.info(`[TikTokConnector] Updating price for ${externalId} to ${newPrice}`);
if (!this.appKey || this.appKey === '') {
return true;
}
try {
await axios.post(`${this.apiBaseUrl}/api/products/update_price`, {
shop_id: shopId,
product_id: externalId,
price: newPrice
});
return true;
} catch (error: any) {
logger.error(`[TikTokConnector] Update price failed: ${error.message}`);
return false;
}
}
/**
* 拉取店铺商品
*/
async pullProducts(shopId: string, params?: any): Promise<PlatformProduct[]> {
logger.info(`[TikTokConnector] Pulling products for shop: ${shopId}`);
return []; // Mock implementation
}
/**
* 推送商品上架
*/
async pushListing(shopId: string, product: PlatformProduct): Promise<PublishResult> {
logger.info(`[TikTokConnector] Pushing listing for shop: ${shopId}, product: ${product.title}`);
return { success: true, externalId: `tk_${Date.now()}`, publishUrl: 'https://shop.tiktok.com/...' };
}
/**
* 更新商品价格
*/
async updatePrice(shopId: string, externalId: string, newPrice: number): Promise<boolean> {
logger.info(`[TikTokConnector] Updating price for shop: ${shopId}, product: ${externalId} to ${newPrice}`);
return true;
}
/**
* 同步库存
*/
async syncInventory(shopId: string, externalId: string, quantity: number): Promise<boolean> {
logger.info(`[TikTokConnector] Syncing inventory for shop: ${shopId}, product: ${externalId} to ${quantity}`);
return true;
logger.info(`[TikTokConnector] Syncing inventory for ${externalId}: ${quantity}`);
if (!this.appKey || this.appKey === '') {
return true;
}
try {
await axios.post(`${this.apiBaseUrl}/api/products/update_stock`, {
shop_id: shopId,
product_id: externalId,
stock: quantity
});
return true;
} catch (error: any) {
logger.error(`[TikTokConnector] Sync inventory failed: ${error.message}`);
return false;
}
}
async calculateShipping(shopId: string, params: { countryCode: string; weight: number }): Promise<{
carrier: string;
cost: number;
currency: string;
estimatedDays: number;
}> {
const baseCost = params.countryCode === 'US' ? 3.99 : 8.99;
const weightCost = Math.max(0, (params.weight - 0.5) * 2);
return {
carrier: 'TikTok Shipping',
cost: baseCost + weightCost,
currency: 'USD',
estimatedDays: params.countryCode === 'US' ? 3 : 10
};
}
private mapToPlatformProduct(tiktokProduct: TikTokProduct): PlatformProduct {
return {
externalId: tiktokProduct.product_id,
title: tiktokProduct.title,
description: tiktokProduct.description,
price: parseFloat(tiktokProduct.price.original_price),
currency: tiktokProduct.price.currency || 'USD',
images: [tiktokProduct.main_image, ...tiktokProduct.images],
skus: tiktokProduct.skus.map(sku => ({
skuId: sku.id,
price: parseFloat(sku.price),
stock: sku.stock,
specs: sku.specs
})),
platform: 'TIKTOK',
category: tiktokProduct.category_id,
attributes: tiktokProduct.attributes
};
}
private mapFromPlatformProduct(product: PlatformProduct): Partial<TikTokProduct> {
return {
title: product.title,
description: product.description || '',
price: {
original_price: product.price.toString(),
currency: product.currency || 'USD'
},
main_image: product.images[0] || '',
images: product.images.slice(1),
category_id: product.category || '',
attributes: product.attributes || {}
};
}
private getMockProducts(): PlatformProduct[] {
return [
{
externalId: 'TK_PROD_001',
title: 'TikTok Viral LED Ring Light',
description: 'Professional 10-inch LED ring light with tripod stand, perfect for TikTok videos and live streaming.',
price: 29.99,
currency: 'USD',
images: [
'https://example.com/tiktok/ring-light-1.jpg',
'https://example.com/tiktok/ring-light-2.jpg'
],
skus: [
{ skuId: 'TK_SKU_001_W', price: 29.99, stock: 150, specs: { color: 'White' } },
{ skuId: 'TK_SKU_001_B', price: 29.99, stock: 100, specs: { color: 'Black' } }
],
platform: 'TIKTOK',
category: 'Electronics',
attributes: { brand: 'TikTokViral', material: 'ABS Plastic' }
},
{
externalId: 'TK_PROD_002',
title: 'Trendy Phone Grip Stand',
description: 'Cute and trendy phone grip stand, perfect accessory for content creators.',
price: 9.99,
currency: 'USD',
images: ['https://example.com/tiktok/phone-grip.jpg'],
skus: [
{ skuId: 'TK_SKU_002', price: 9.99, stock: 500, specs: {} }
],
platform: 'TIKTOK',
category: 'Accessories'
}
];
}
private getMockOrders(): TikTokOrder[] {
return [
{
order_id: 'TK_ORDER_001',
order_status: 'SHIPPED',
create_time: Math.floor(Date.now() / 1000) - 3600,
buyer_info: {
user_id: 'TK_USER_001',
username: 'tiktok_buyer_1'
},
items: [
{
product_id: 'TK_PROD_001',
sku_id: 'TK_SKU_001_W',
quantity: 1,
price: '29.99'
}
],
shipping_info: {
address: '123 Main St',
city: 'Los Angeles',
state: 'CA',
country: 'US',
zip_code: '90001'
},
total_amount: '35.98',
currency: 'USD'
}
];
}
}
export default TikTokConnector;

View File

@@ -1,40 +1,114 @@
import { logger } from '../../utils/logger';
import { FeatureGovernanceService } from '../governance/FeatureGovernanceService';
import { SERVICE_CONFIGS, ServiceConfig } from '../../config/serviceConfig';
export interface DomainModule {
name: string;
priority: number;
init: () => Promise<void>;
isAgi?: boolean; // 是否属于重型 AGI 模块
isAgi?: boolean;
config?: ServiceConfig;
}
/**
* [ARCH_LIGHT_01] 领域注册中心 (Domain Registry)
* @description 核心逻辑:解耦 index.ts 中的服务初始化,支持按优先级、按领域、按 AGI 降级策略进行加载。
*/
class ServiceStateManager {
private static instance: ServiceStateManager;
private enabledServices: Set<string>;
private initializedServices: Set<string> = new Set();
private failedServices: Map<string, string> = new Map();
private constructor() {
this.enabledServices = new Set(
SERVICE_CONFIGS.filter(s => s.enabled).map(s => s.name)
);
}
static getInstance(): ServiceStateManager {
if (!this.instance) {
this.instance = new ServiceStateManager();
}
return this.instance;
}
isEnabled(name: string): boolean {
return this.enabledServices.has(name);
}
enable(name: string): void {
this.enabledServices.add(name);
}
disable(name: string): void {
this.enabledServices.delete(name);
}
markInitialized(name: string): void {
this.initializedServices.add(name);
this.failedServices.delete(name);
}
markFailed(name: string, error: string): void {
this.failedServices.set(name, error);
}
getStatus(): {
enabled: string[];
initialized: string[];
failed: Map<string, string>;
} {
return {
enabled: Array.from(this.enabledServices),
initialized: Array.from(this.initializedServices),
failed: new Map(this.failedServices),
};
}
getAllServices(): Array<{
name: string;
enabled: boolean;
initialized: boolean;
error?: string;
config?: ServiceConfig;
}> {
return SERVICE_CONFIGS.map(config => ({
name: config.name,
enabled: this.enabledServices.has(config.name),
initialized: this.initializedServices.has(config.name),
error: this.failedServices.get(config.name),
config,
}));
}
setEnabledServices(services: string[]): void {
this.enabledServices = new Set(services);
}
}
export const serviceStateManager = ServiceStateManager.getInstance();
export class DomainRegistry {
private static modules: DomainModule[] = [];
/**
* 注册领域模块
*/
static register(module: DomainModule) {
this.modules.push(module);
const config = SERVICE_CONFIGS.find(s => s.name === module.name);
this.modules.push({ ...module, config });
}
/**
* 执行全量初始化
*/
static async bootstrap(tenantId?: string) {
// 1. 按优先级排序 (低值优先)
const sortedModules = [...this.modules].sort((a, b) => a.priority - b.priority);
// 2. 检查 AGI 基础模式开关 (如果开启,则跳过重型 AGI 模块)
const isAgiBaseMode = await FeatureGovernanceService.isEnabled('CORE_AGI_BASE_MODE', tenantId);
logger.info(`[DomainRegistry] Bootstrapping ${sortedModules.length} modules... (AGI Base Mode: ${isAgiBaseMode})`);
const state = ServiceStateManager.getInstance();
const enabledCount = sortedModules.filter(m => state.isEnabled(m.name)).length;
logger.info(`[DomainRegistry] Bootstrapping ${enabledCount}/${sortedModules.length} modules... (AGI Base Mode: ${isAgiBaseMode})`);
for (const module of sortedModules) {
if (!state.isEnabled(module.name)) {
logger.info(`[DomainRegistry] Skipping disabled module: ${module.name}`);
continue;
}
if (module.isAgi && isAgiBaseMode) {
logger.warn(`[DomainRegistry] Skipping Heavy AGI module: ${module.name} (Base Mode Active)`);
continue;
@@ -42,33 +116,47 @@ export class DomainRegistry {
try {
await module.init();
state.markInitialized(module.name);
logger.info(`[DomainRegistry] Module initialized: ${module.name}`);
} catch (err: any) {
logger.error(`[DomainRegistry] Module ${module.name} failed to initialize: ${err.message}`);
// 核心模块失败应抛出异常,非核心模块可继续
if (module.priority < 10) throw err;
} catch (err: unknown) {
const errorMessage = err instanceof Error ? err.message : String(err);
logger.error(`[DomainRegistry] Module ${module.name} failed to initialize: ${errorMessage}`);
state.markFailed(module.name, errorMessage);
if (module.priority < 10) {
throw err;
}
}
}
logger.info('✅ [DomainRegistry] Bootstrap completed successfully.');
}
/**
* 预定义优先级常量
*/
static Priority = {
CORE_INFRA: 0, // 核心基础设施 (DB, Cache, Governance)
SECURITY: 5, // 安全与身份 (Auth, DID, ZKP)
RUNTIME: 10, // 运行时引擎 (AsyncEngine, PluginMgr)
BIZ_DOMAIN: 20, // 业务领域 (Orders, Products, Trade)
AGI_HEAVY: 50, // 重型 AGI (Evolution, RCA, XAI)
SUPPORT: 100 // 辅助支撑 (Logistics, Tax, Sync)
CORE_INFRA: 0,
SECURITY: 5,
RUNTIME: 10,
BIZ_DOMAIN: 20,
AGI_HEAVY: 50,
SUPPORT: 100
};
/**
* 获取所有已注册的领域模块
*/
static getDomains(): DomainModule[] {
return [...this.modules];
}
static getServiceStatus() {
return ServiceStateManager.getInstance().getAllServices();
}
static setEnabledServices(services: string[]): void {
ServiceStateManager.getInstance().setEnabledServices(services);
}
static enableService(name: string): void {
ServiceStateManager.getInstance().enable(name);
}
static disableService(name: string): void {
ServiceStateManager.getInstance().disable(name);
}
}