/** * 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} */ async fetchMetrics(dateRange) { return null; } /** @returns {Promise} */ async fetchContent(contentId) { return null; } /** @returns {Promise} */ 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 };