datasource-service.js 8.36 KB
/**
 * DataSourceService — 运营数据接入抽象层
 *
 * 提供标准化的数据源接口,支持多平台运营数据接入。
 * 每个数据源实现 DataSourceAdapter 接口:
 *   - fetchMetrics(dateRange) → 获取核心指标
 *   - fetchContent(contentId) → 获取内容详情
 *   - fetchTrending(params) → 获取热点/趋势
 *
 * 已实现的 Adapter:
 *   - MockAdapter: 本地模拟数据 (开发调试用)
 *   - FileAdapter: 从本地文件读取数据 (CSV/JSON)
 *
 * 可扩展:
 *   - WechatAdapter: 微信公众号后台数据
 *   - DouyinAdapter: 抖音数据
 *   - CustomAPIAdapter: 自定义 REST API
 */

const fs = require('fs');
const path = require('path');

const DATA_DIR = path.resolve(__dirname, '../../data');

// ════════════════════════════════════════
//  DataSource Adapter 基类
// ════════════════════════════════════════

class DataSourceAdapter {
  constructor(config = {}) {
    this.config = config;
    this.name = 'base';
  }

  /** @returns {Promise<any>} */
  async fetchMetrics(dateRange) { return null; }
  /** @returns {Promise<any>} */
  async fetchContent(contentId) { return null; }
  /** @returns {Promise<any>} */
  async fetchTrending(params) { return null; }
}

// ════════════════════════════════════════
//  Mock Adapter — 模拟数据 (开发调试用)
// ════════════════════════════════════════

class MockAdapter extends DataSourceAdapter {
  constructor(config) {
    super(config);
    this.name = 'mock';
  }

  async fetchMetrics({ startDate, endDate }) {
    const days = [];
    const start = new Date(startDate);
    const end = new Date(endDate);
    for (let d = new Date(start); d <= end; d.setDate(d.getDate() + 1)) {
      days.push({
        date: d.toISOString().slice(0, 10),
        views: Math.floor(Math.random() * 10000 + 500),
        likes: Math.floor(Math.random() * 500 + 20),
        shares: Math.floor(Math.random() * 100 + 5),
        comments: Math.floor(Math.random() * 200 + 10),
        followers: Math.floor(Math.random() * 50 + 1),
        revenue: Math.round(Math.random() * 5000) / 100,
      });
    }
    return {
      source: 'mock',
      period: { startDate, endDate },
      dailyMetrics: days,
      summary: {
        totalViews: days.reduce((s, d) => s + d.views, 0),
        totalLikes: days.reduce((s, d) => s + d.likes, 0),
        avgDailyViews: Math.round(days.reduce((s, d) => s + d.views, 0) / days.length),
      },
    };
  }

  async fetchTrending({ platform = 'weibo', count = 10 }) {
    const topics = [
      'AI 革命新进展', '年轻人消费降级', '职场新趋势', '健康科技创新',
      '城市生活方式', '教育改革方向', '新能源汽车竞争', '社交媒体新玩法',
      '房市最新政策', '投资理财策略', '远程办公生态', '文化IP跨界',
    ];
    return {
      source: 'mock',
      platform,
      timestamp: new Date().toISOString(),
      topics: topics.slice(0, count).map((t, i) => ({
        rank: i + 1,
        title: t,
        hotness: Math.floor(Math.random() * 1000000 + 100000),
        category: ['tech', 'lifestyle', 'finance', 'culture'][i % 4],
      })),
    };
  }
}

// ════════════════════════════════════════
//  File Adapter — 从本地文件读取数据
// ════════════════════════════════════════

class FileAdapter extends DataSourceAdapter {
  constructor(config = {}) {
    super(config);
    this.name = 'file';
    this.dataDir = (config && config.dataDir) || path.join(DATA_DIR, '_datasources');
  }

  async fetchMetrics({ startDate, endDate }) {
    const metricsPath = path.join(this.dataDir, 'metrics.json');
    if (!fs.existsSync(metricsPath)) return { source: 'file', error: 'metrics.json not found' };
    const allData = JSON.parse(fs.readFileSync(metricsPath, 'utf-8'));

    // 按日期过滤
    const filtered = (allData.dailyMetrics || [])
      .filter(d => d.date >= startDate && d.date <= endDate);

    return {
      source: 'file',
      period: { startDate, endDate },
      dailyMetrics: filtered,
      summary: {
        totalViews: filtered.reduce((s, d) => s + (d.views || 0), 0),
        totalLikes: filtered.reduce((s, d) => s + (d.likes || 0), 0),
        avgDailyViews: filtered.length > 0
          ? Math.round(filtered.reduce((s, d) => s + (d.views || 0), 0) / filtered.length)
          : 0,
      },
    };
  }

  async fetchTrending({ platform = 'local', count = 10 }) {
    const trendingPath = path.join(this.dataDir, `trending_${platform}.json`);
    if (!fs.existsSync(trendingPath)) {
      // fallback to generic trending.json
      const genericPath = path.join(this.dataDir, 'trending.json');
      if (!fs.existsSync(genericPath)) return { source: 'file', error: 'trending data not found' };
      return JSON.parse(fs.readFileSync(genericPath, 'utf-8'));
    }
    return JSON.parse(fs.readFileSync(trendingPath, 'utf-8'));
  }
}

// ════════════════════════════════════════
//  DataSourceService — 统一管理入口
// ════════════════════════════════════════

class DataSourceService {
  constructor() {
    this.adapters = new Map();
    // 默认注册 Mock 适配器
    this.register('mock', new MockAdapter());
    this.register('file', new FileAdapter());
  }

  /**
   * 注册数据源适配器
   */
  register(name, adapter) {
    this.adapters.set(name, adapter);
  }

  /**
   * 获取数据源
   */
  getAdapter(name) {
    return this.adapters.get(name);
  }

  /**
   * 获取指标数据 (自动选择适配器)
   * @param {string} source - 'mock' | 'file' | 'wechat' | ...
   * @param {object} params - { startDate, endDate }
   */
  async fetchMetrics(source, params) {
    const adapter = this.adapters.get(source);
    if (!adapter) throw new Error(`DataSource adapter "${source}" not found`);
    return adapter.fetchMetrics(params);
  }

  /**
   * 获取热点趋势
   */
  async fetchTrending(source, params) {
    const adapter = this.adapters.get(source);
    if (!adapter) throw new Error(`DataSource adapter "${source}" not found`);
    return adapter.fetchTrending(params);
  }

  /**
   * 归因分析 — 对比两个时间段的指标变化
   *
   * @param {string} source
   * @param {object} current - { startDate, endDate } 当前周期
   * @param {object} previous - { startDate, endDate } 上一周期
   * @returns {object} 归因分析结果
   */
  async analyzeAttribution(source, current, previous) {
    const currentData = await this.fetchMetrics(source, current);
    const previousData = await this.fetchMetrics(source, previous);

    const cs = currentData.summary;
    const ps = previousData.summary;

    const changes = {};
    for (const key of Object.keys(cs)) {
      const curr = cs[key] || 0;
      const prev = ps[key] || 0;
      const delta = prev > 0 ? ((curr - prev) / prev * 100).toFixed(1) : 'N/A';
      changes[key] = {
        current: curr,
        previous: prev,
        changePercent: delta,
        trend: curr > prev ? '📈 上升' : curr < prev ? '📉 下降' : '➡️ 持平',
      };
    }

    // 找出最显著的变化维度
    const significantChanges = Object.entries(changes)
      .filter(([_, v]) => v.changePercent !== 'N/A' && Math.abs(parseFloat(v.changePercent)) > 10)
      .sort((a, b) => Math.abs(parseFloat(b[1].changePercent)) - Math.abs(parseFloat(a[1].changePercent)));

    return {
      source,
      currentPeriod: current,
      previousPeriod: previous,
      changes,
      insights: significantChanges.map(([key, val]) =>
        `${key}: ${val.trend} ${val.changePercent}% (${val.previous} → ${val.current})`
      ),
      recommendation: significantChanges.length > 0
        ? `主要变化维度: ${significantChanges[0][0]} (${significantChanges[0][1].changePercent}%)`
        : '各项指标基本持平,无显著变化',
    };
  }
}

module.exports = { DataSourceService, DataSourceAdapter, MockAdapter, FileAdapter };