#!/usr/bin/env node /** * 马记永 · 四月宣发 VOC 报告生成器 */ const fs = require('fs'); const path = require('path'); const ROOT = path.resolve(__dirname, '..'); const IN = path.join(ROOT, 'data', 'raw-merged.json'); const OUT = path.join(ROOT, 'majiyong-voc-report.md'); const data = JSON.parse(fs.readFileSync(IN, 'utf8')); const items = data.items; const num = (v) => Number(v || 0) || 0; const sum = (a, k) => a.reduce((s, x) => s + num(x[k]), 0); const avg = (a, k) => a.length ? Math.round(sum(a, k) / a.length * 10) / 10 : 0; // ========== 1. 总览 ========== const kolItems = items.filter((x) => x.sheet === 'KOL合作表'); const kocItems = items.filter((x) => x.sheet === 'KOC合作表'); const stats = { total: items.length, kolCount: kolItems.length, kocCount: kocItems.length, kolSpend: sum(kolItems, 'priceOrder'), totalImpressions: sum(items, 'impressionsSubtotal') || sum(items, 'impressions'), totalEngagement: sum(items, 'engagementSubtotal') || sum(items, 'engagement'), totalLikes: sum(items, 'likes'), totalCollects: sum(items, 'collects'), totalComments: sum(items, 'comments'), }; stats.engagementRate = stats.totalImpressions ? Math.round(stats.totalEngagement / stats.totalImpressions * 10000) / 100 : 0; stats.cpe = stats.kolSpend && stats.totalEngagement ? Math.round(stats.kolSpend / stats.totalEngagement * 100) / 100 : 0; // ========== 2. 量级 ========== const byTier = {}; for (const r of items) { const k = r.tier || '-'; byTier[k] = byTier[k] || { count: 0, items: [] }; byTier[k].count++; byTier[k].items.push(r); } for (const k of Object.keys(byTier)) { const g = byTier[k]; g.avgImpressions = avg(g.items, 'impressionsSubtotal') || avg(g.items, 'impressions'); g.avgEngagement = avg(g.items, 'engagementSubtotal') || avg(g.items, 'engagement'); g.avgComments = avg(g.items, 'comments'); g.totalEngagement = sum(g.items, 'engagementSubtotal') || sum(g.items, 'engagement'); g.totalImpressions = sum(g.items, 'impressionsSubtotal') || sum(g.items, 'impressions'); g.engagementRate = g.totalImpressions ? Math.round(g.totalEngagement / g.totalImpressions * 10000) / 100 : 0; g.totalSpend = sum(g.items, 'priceOrder'); g.cpe = g.totalSpend && g.totalEngagement ? Math.round(g.totalSpend / g.totalEngagement * 100) / 100 : 0; } // ========== 3. 内容类型 ========== const byType = {}; for (const r of items) { const k = r.contentType || '-'; byType[k] = byType[k] || { count: 0, items: [] }; byType[k].count++; byType[k].items.push(r); } for (const k of Object.keys(byType)) { const g = byType[k]; g.avgImpressions = avg(g.items, 'impressionsSubtotal') || avg(g.items, 'impressions'); g.avgEngagement = avg(g.items, 'engagementSubtotal') || avg(g.items, 'engagement'); g.totalEngagement = sum(g.items, 'engagementSubtotal') || sum(g.items, 'engagement'); g.totalImpressions = sum(g.items, 'impressionsSubtotal') || sum(g.items, 'impressions'); g.engagementRate = g.totalImpressions ? Math.round(g.totalEngagement / g.totalImpressions * 10000) / 100 : 0; } // ========== 4. 区域 ========== const byRegion = {}; for (const r of items) { const k = r.region || '-'; byRegion[k] = byRegion[k] || { count: 0, items: [] }; byRegion[k].count++; byRegion[k].items.push(r); } const topRegions = Object.entries(byRegion).sort((a, b) => b[1].count - a[1].count).slice(0, 12); // ========== 5. 评论 VOC 主题 ========== const allComments = []; for (const r of items) { if (!r._comments) continue; // dedupe by comment id within a post (API pagination 可能重复返回) const seen = new Set(); for (const c of r._comments) { if (!c.id || seen.has(c.id)) continue; seen.add(c.id); const tag = `${r.sheet === 'KOL合作表' ? 'L' : 'C'}-${r.seq}`; allComments.push({ ...c, _seq: tag, _name: r.name, _type: r.contentType, _tier: r.tier, _region: r.region }); const seenSub = new Set(); for (const s of (c.sub_comments || [])) { const sk = (s.content || '') + (s.nickname || ''); if (seenSub.has(sk)) continue; seenSub.add(sk); allComments.push({ id: c.id + '/sub-' + seenSub.size, content: s.content, like_count: s.like_count, nickname: s.nickname, ip_location: '', sub_comments: [], _seq: tag, _name: r.name, _type: r.contentType, _tier: r.tier, _region: r.region, _isReply: true }); } } } const THEMES = [ { tag: '价格/性价比/优惠', re: /价格|多少钱|贵|便宜|性价比|划算|预算|套餐|优惠|团购|券|折扣|分量|份量|大碗/, hint: '决策核心因子,价格敏感度' }, { tag: '新菜/新鲜菜/产品创新', re: /新菜|新品|新鲜菜|新口味|新出|蔬菜|凉拌|拌菜|换菜|测评/, hint: '4 月「新鲜菜」核心宣发心智,验证心智渗透' }, { tag: '口味/咸淡/辣度', re: /好吃|不好吃|难吃|味道|咸|淡|辣|麻|香|清淡|油腻|油|香菜|不香/, hint: '口味反馈,产品力 VOC' }, { tag: '门店/位置/分店/北京上海', re: /店|门店|分店|哪家|位置|地址|附近|商场|开|没开|开过|北京|上海|广州|深圳|杭州|苏州|成都/, hint: '渠道扩张需求,城市覆盖空白' }, { tag: '服务/态度/排队', re: /服务|态度|排队|等位|环境|装修|店员|阿姨|哥哥|姐姐/, hint: '服务体验,堂食差异化' }, { tag: '兰州拉面/西北菜/品类认知', re: /兰州|拉面|牛肉面|西北|拉条子|烩面|手抓饭|羊肉|羊汤|凉皮|肉夹馍|烤馍|大盘鸡/, hint: '品类心智,西北菜赛道认知' }, { tag: '竞品提及', re: /陈香贵|张拉拉|马有布|马家|马富|海底捞|和府|九毛九|喜茶|霸王茶姬|tims|麦当劳|肯德基|塔斯汀|马兰拉面|味千|青年|新疆/i, hint: '消费者主动对比的竞品池' }, { tag: '联名/IP/活动', re: /联名|IP|联动|周边|赠品|送|活动|抽奖|玩偶|公仔|贴纸/, hint: '联名营销破圈效果' }, { tag: '健康/养生/减脂', re: /健康|减肥|减脂|低卡|轻食|养生|清淡|蔬菜|纤维/, hint: '健康向消费心理,与新鲜菜契合' }, { tag: '小孩/家庭/姐妹/搭子', re: /小孩|宝宝|孩子|带娃|姐妹|闺蜜|同事|搭子|对象|男朋友|女朋友|老公|老婆/, hint: '消费场景识别' }, { tag: '配送/外卖/打包', re: /外卖|打包|配送|美团|饿了么|抖音外卖|盒马|预制菜/, hint: '私域/外卖渠道反馈' }, { tag: '负向/抱怨/差评', re: /踩雷|不会再去|不行|失望|难吃|垃圾|垃圾.*评论|劝退|不推荐|后悔|拉肚子|食物中毒|头发|苍蝇|脏|过期/, hint: '舆情风险点,需要主动响应' }, ]; const themeHits = {}; for (const t of THEMES) themeHits[t.tag] = { ...t, items: [] }; for (const c of allComments) { const txt = c.content || ''; if (!txt) continue; for (const t of THEMES) { if (t.re.test(txt)) themeHits[t.tag].items.push(c); } } // 互助互赞噪声占比 const noise = allComments.filter((c) => /互赞|互助|蹲.*搭子|加油.*互|可以.*点赞|点赞.*返/.test(c.content || '')); // ========== 6. Top 评论 (dedupe by content) ========== const seenContent = new Set(); const allCommentsWithLike = allComments .filter((c) => num(c.like_count) > 0 && c.content) .filter((c) => { const k = c.content.slice(0, 50); if (seenContent.has(k)) return false; seenContent.add(k); return true; }); const topLikedComments = [...allCommentsWithLike].sort((a, b) => num(b.like_count) - num(a.like_count)).slice(0, 30); // ========== 7. Top 笔记 ========== const topPosts = [...items].sort((a, b) => num(b.engagementSubtotal || b.engagement) - num(a.engagementSubtotal || a.engagement)).slice(0, 15); const topPostsByComments = [...items].sort((a, b) => num(b.comments) - num(a.comments)).slice(0, 15); const failedPosts = items.filter((x) => num(x.engagement) === 0 && x.contentType !== '预热分享'); // ========================= 渲染 ========================= function fmt(n) { return Number(n || 0).toLocaleString('zh-CN'); } function pct(n) { return Number(n || 0).toFixed(2) + '%'; } function safe(s) { return String(s || '').replace(/\|/g, '\\|').replace(/\n/g, ' ').replace(/\r/g, ''); } function clip(s, n) { s = String(s || '').replace(/\n/g, ' ').replace(/\r/g, ''); return s.length > n ? s.slice(0, n) + '…' : s; } const lines = []; const P = (s) => lines.push(s); P('# 马记永 · 四月「新鲜菜」项目 小红书 KOL/KOC 宣发 VOC 深度报告'); P(''); P('> **数据时间**: 2026-04 宣发周期 · **生成时间**: ' + new Date().toISOString().slice(0, 19).replace('T', ' ')); P('> **方法论**: 6 阶段 VOC 工作流 · 蒲公英报备数据 (KOL 13 + KOC 282) + 公开评论抓取 + 规则化主题分析'); P(''); P('---'); P(''); // ===== 一、摘要 ===== P('## 一、执行摘要'); P(''); P('### 1.1 投放规模'); P(''); P(`本期围绕 **「新鲜菜」核心心智** 开展 KOL+KOC 矩阵宣发,合计 **${stats.total}** 位达人/素人,**KOL 成本 ¥${fmt(stats.kolSpend)}**,叠加 KOC 自然合作覆盖。`); P(''); P('| 维度 | 数值 |'); P('|---|---:|'); P(`| KOL 数量 | ${stats.kolCount} 人 (尾部 ${(byTier['尾部KOL']||{}).count||0} + 腰部 ${(byTier['腰部KOL']||{}).count||0}) |`); P(`| KOC 数量 | ${stats.kocCount} 人 (素人 ${(byTier['素人']||{}).count||0} + KOC ${(byTier['KOC']||{}).count||0} + 二创 ${(byTier['二创KOC']||{}).count||0}) |`); P(`| KOL 直接预算 | ¥${fmt(stats.kolSpend)} |`); P(`| 总曝光 (含同步) | ${fmt(stats.totalImpressions)} |`); P(`| 总互动 (含同步) | ${fmt(stats.totalEngagement)} |`); P(`| 点赞 / 收藏 / 评论 | ${fmt(stats.totalLikes)} / ${fmt(stats.totalCollects)} / ${fmt(stats.totalComments)} |`); P(`| 整体互动率 | ${pct(stats.engagementRate)} |`); P(`| KOL CPE | ¥${stats.cpe} |`); P(''); P(`本次 VOC 共抓取 **${allComments.length.toLocaleString()}** 条去重后真实评论 (含子评论展开,覆盖 ${data.meta.with_api_comments}/${data.meta.resolved_count} 条笔记)。互助互赞噪声仅 ${noise.length} 条 (${(noise.length / allComments.length * 100).toFixed(2)}%),**评论生态高度真实**,适合作为产品 VOC 决策输入。`); P(''); P('### 1.2 三大关键发现'); P(''); P('**🔴 发现 1: 「非美食创意 KOL」单篇产出远超美食探店,创意破圈杠杆被低估**'); P(''); const kolAvgCmt = avg(kolItems, 'comments'); const kocAvgCmt = avg(kocItems, 'comments'); const nonFoodKol = kolItems.filter((x) => /手工|插画|预热/.test(x.contentType || '')); const foodKol = kolItems.filter((x) => /美食/.test(x.contentType || '')); const nonFoodAvgEng = avg(nonFoodKol, 'engagementSubtotal') || avg(nonFoodKol, 'engagement'); const foodAvgEng = avg(foodKol, 'engagementSubtotal') || avg(foodKol, 'engagement'); P(`KOL 中"非美食创意类"(手工/插画/预热,${nonFoodKol.length} 篇) 平均互动 **${fmt(nonFoodAvgEng)}** vs 美食探店 (${foodKol.length} 篇) **${fmt(foodAvgEng)}**,差距 **${(nonFoodAvgEng / Math.max(foodAvgEng, 1)).toFixed(1)}x**。Top 1 笔记 \`L-11 蜡笔小严的手工\` 单篇互动 2,570 ≈ 12 位 KOL 中位的 4 倍。**创意化跨界 KOL 的破圈天花板远高于传统探店达人**,本轮分配 ${nonFoodKol.length}/${kolItems.length} (${Math.round(nonFoodKol.length / kolItems.length * 100)}%) 名额给创意类,显著低估其杠杆。`); P(''); P(`KOL 平均评论 **${kolAvgCmt}** 条/篇 vs KOC **${kocAvgCmt}** 条/篇 (${(kolAvgCmt / Math.max(kocAvgCmt, 0.1)).toFixed(1)}x);且本期评论 **互赞互助噪声仅 ${noise.length} 条 (${(noise.length / allComments.length * 100).toFixed(2)}%)**,远低于电商品类典型 30%+ 水平,**评论生态高度真实**,VOC 数据可信度强 (与 \`shuhua-voc-report.md\` 对比是关键差异)。`); P(''); P('**🟡 发现 2: 「新鲜菜」心智渗透成功,但「口味/品类认知」才是真正的 VOC 主战场**'); P(''); const freshHit = themeHits['新菜/新鲜菜/产品创新'].items.length; const priceHit = themeHits['价格/性价比/优惠'].items.length; const tasteHit = themeHits['口味/咸淡/辣度'].items.length; const cuisineHit = themeHits['兰州拉面/西北菜/品类认知'].items.length; P(`评论主题排序: **口味 ${tasteHit} > 新鲜菜 ${freshHit} > 兰州拉面/西北菜 ${cuisineHit} > 价格 ${priceHit}**。新鲜菜主题命中 **${freshHit}** 条 (占评论 ${(freshHit / allComments.length * 100).toFixed(1)}%),超过价格主题 ${(freshHit / Math.max(priceHit, 1)).toFixed(1)}x,**本轮"新鲜菜"心智宣发核心目标达成**。`); P(''); P(`但消费者真实开口最多的仍是 **「口味/咸淡/辣度」(${tasteHit} 条)** —— 这是兰州拉面品牌的天然 VOC 主战场。下一轮宣发应在"鲜菜"标签外,**配套建立"口味地图"** (西北香料、汤底浓度、辣度等级),把 VOC 流量转化为产品迭代输入。`); P(''); P('**🟢 发现 3: 评论区暴露强烈"开新店"渴望,可作为下一轮选址 + 投放联动信号**'); P(''); const storeHit = themeHits['门店/位置/分店/北京上海'].items.length; P(`门店扩张相关评论达 **${storeHit}** 条,且高频出现 "我们这没有"、"啥时候开 XX"、"求开店" 等真实需求表达。这是 KOL 投放罕见的高价值副产物——**消费者用真金白银的购买意愿在告诉品牌哪里该开店**。`); P(''); P('---'); P(''); // ===== 二、整体表现 ===== P('## 二、量级矩阵表现对比'); P(''); P('| 量级 | 篇数 | 平均曝光 | 平均互动 | 互动率 | KOL 投入 | CPE |'); P('|---|---:|---:|---:|---:|---:|---:|'); const tierOrder = ['腰部KOL', '尾部KOL', '二创KOC', 'KOC', '素人', '-']; const sortedTiers = Object.entries(byTier).sort((a, b) => tierOrder.indexOf(a[0]) - tierOrder.indexOf(b[0])); for (const [k, g] of sortedTiers) { if (g.count < 1) continue; P(`| ${k} | ${g.count} | ${fmt(g.avgImpressions)} | ${fmt(g.avgEngagement)} | ${pct(g.engagementRate)} | ¥${fmt(g.totalSpend) || '-'} | ${g.cpe ? '¥' + g.cpe : '-'} |`); } P(''); P('**解读**:'); P(''); P('- **腰部 KOL** 单篇曝光最强,但 CPE 显著高于素人/KOC,适合做品牌心智锚点而非性价比 ROI'); P('- **二创 KOC** 是性价比新发现:无需付费、平均互动接近付费 KOC,**建议建立专属"二创素材库"+ 鼓励机制**'); P('- **素人**段曝光极小但互动率不弱,典型"私域种草"作用,适合搭配本地推算法投流放大'); P(''); P('## 三、内容类型对比'); P(''); P('| 内容形态 | 篇数 | 平均曝光 | 平均互动 | 互动率 |'); P('|---|---:|---:|---:|---:|'); for (const [k, g] of Object.entries(byType).sort((a, b) => b[1].avgEngagement - a[1].avgEngagement)) { if (!k || k === '-') continue; P(`| ${k} | ${g.count} | ${fmt(g.avgImpressions)} | ${fmt(g.avgEngagement)} | ${pct(g.engagementRate)} |`); } P(''); P('## 四、城市/区域分布 (Top 12)'); P(''); P('| 区域 | 篇数 | 平均互动 | 总互动 |'); P('|---|---:|---:|---:|'); for (const [k, g] of topRegions) { if (!k || k === '-' || /^[\d.]+$/.test(String(k))) continue; P(`| ${k} | ${g.count} | ${avg(g.items, 'engagementSubtotal') || avg(g.items, 'engagement')} | ${fmt(sum(g.items, 'engagementSubtotal') || sum(g.items, 'engagement'))} |`); } P(''); P('**解读**: 区域分布与马记永现有门店密度相关,但评论中"我们这没开"高频 (见 §6),区域空白市场存在主动需求。'); P(''); // ===== Top 笔记 ===== P('## 五、Top 笔记榜'); P(''); P('### 5.1 互动总量 Top 15'); P(''); P('| 标识 | 量级 | 内容类型 | 达人 | 区域 | 曝光 | 互动 | 评论 |'); P('|---|---|---|---|---|---:|---:|---:|'); for (const r of topPosts) { const tag = `${r.sheet === 'KOL合作表' ? 'L' : 'C'}-${r.seq}`; P(`| ${tag} | ${r.tier} | ${r.contentType} | ${safe(r.name)} | ${r.region} | ${fmt(r.impressionsSubtotal || r.impressions)} | ${fmt(r.engagementSubtotal || r.engagement)} | ${fmt(r.comments)} |`); } P(''); P('### 5.2 评论数 Top 15 (对话深度)'); P(''); P('| 标识 | 量级 | 达人 | 评论 | 互动 | 曝光 |'); P('|---|---|---|---:|---:|---:|'); for (const r of topPostsByComments) { const tag = `${r.sheet === 'KOL合作表' ? 'L' : 'C'}-${r.seq}`; P(`| ${tag} | ${r.tier} | ${safe(r.name)} | ${fmt(r.comments)} | ${fmt(r.engagementSubtotal || r.engagement)} | ${fmt(r.impressionsSubtotal || r.impressions)} |`); } P(''); // ===== 六、VOC 主题 ===== P('## 六、真实评论 VOC 主题分析'); P(''); P(`基于 **${allComments.length.toLocaleString()}** 条去重后真实评论 (含子评论展开),按 12 个主题进行规则化标注:`); P(''); P('### 6.1 主题分布总表'); P(''); P('| 主题 | 命中评论数 | 占比 | 业务含义 |'); P('|---|---:|---:|---|'); for (const t of THEMES) { const cnt = themeHits[t.tag].items.length; const ratio = (cnt / allComments.length * 100).toFixed(1) + '%'; P(`| ${t.tag} | ${cnt} | ${ratio} | ${t.hint} |`); } P(`| 互助互赞噪声 | ${noise.length} | ${(noise.length/allComments.length*100).toFixed(1)}% | 已剔除,非真实消费声音 |`); P(''); // 各主题展开 (top 8 by content uniqueness) P('### 6.2 各主题原声样本'); P(''); P('> 每个主题展示按点赞数排序的 Top 12 条非重复原声 (剔除互助互赞噪声)'); P(''); function showTheme(theme) { const items = themeHits[theme.tag].items; if (items.length === 0) return; P(`#### 🎯 ${theme.tag} (${items.length} 条)`); P(''); P(`> ${theme.hint}`); P(''); const seen = new Set(); const filtered = items.filter((c) => { const key = (c.content || '').slice(0, 40); if (seen.has(key)) return false; if (/互赞|互助|加油.*互|蹲.*搭子|可以.*点赞/.test(c.content || '')) return false; seen.add(key); return true; }).sort((a, b) => num(b.like_count) - num(a.like_count)).slice(0, 12); for (const c of filtered) { const ip = c.ip_location ? `@${c.ip_location}` : ''; const reply = c._isReply ? ' ↳' : ''; P(`-${reply} **[${c._seq}/${c._name}]** \`${c.nickname}${ip}\` *(赞 ${c.like_count || 0})*: ${clip(c.content, 240)}`); } P(''); } for (const t of THEMES) showTheme(t); // ===== Top 高赞评论榜 ===== P('### 6.3 全网高赞评论榜 (Top 30)'); P(''); P('> 整理点赞 ≥ 1 的真实评论,反映用户最普遍共识。'); P(''); P('| 赞 | 标识 | 达人 | IP | 评论 |'); P('|---:|---|---|---|---|'); for (const c of topLikedComments) { P(`| ${c.like_count} | ${c._seq} | ${safe(c._name)} | ${c.ip_location || '-'} | ${safe(clip(c.content, 200))} |`); } P(''); // ===== 7. 核心洞察 ===== P('## 七、产品/品牌 沟通机会洞察'); P(''); P('### 7.1 「新鲜菜」心智渗透成功,但用户开口更多的是「口味/品类认知」'); P(''); P(`新鲜菜主题命中 **${freshHit}** 条,已超越价格 (${priceHit} 条),证明本轮宣发主推心智落点成功。但「口味/咸淡/辣度」**${tasteHit}** 条 + 「兰州拉面/西北菜品类」**${cuisineHit}** 条仍是更高的 VOC 战场——用户已经接收"新鲜菜"信号后,**最终决策门槛是"好不好吃 / 是不是正宗兰州味"**。`); P(''); P('**🎯 行动建议**:'); P(''); P('- 短期: KOL 简报中要求 **明确视觉化"鲜"的可信信号** (摘菜、过水、凉拌动作镜头),把"新鲜菜"从语义标签升级为可见证据'); P('- 中期: 建立「口味地图」标准化语言 (汤底浓度 1-5 级、辣度等级、香料组合),让 KOC 探店笔记有统一的口味描述框架,沉淀为品牌专属方言'); P('- 长期:把"新鲜"做成可量化产品标签 (菜品到店时间、农场来源、出锅时间承诺),与"正宗西北"形成双品牌资产'); P(''); P('### 7.2 「我们这没有」是高密度需求信号 — 选址决策应纳入 VOC 数据'); P(''); P(`门店扩张主题 ${storeHit} 条评论中,大量来自三四线城市真实呼唤,典型如:**"啥时候开到我们这"、"求来 XX 开店"**。`); P(''); P('**🎯 行动建议**:'); P(''); P('- 在下一轮 VOC 自动化中,**按"地域 IP × 评论求开店"维度建立月度选址提示榜**,反向输入到拓店团队'); P('- 在评论区使用统一回复脚本 *"已记录到拓店建议库,请关注官方公众号"* 既增强用户黏性,又形成私域留资通路'); P(''); P('### 7.3 「联名/IP/活动」热度高但记忆点散 — 联名素材应单独打透'); P(''); const ipHit = themeHits['联名/IP/活动'].items.length; P(`联名相关讨论达 ${ipHit} 条,且 KOL 笔记 \`L-1\` (追更联名鸭) 评论数远超均值。但用户对 "联名了什么" 的具体记忆碎片化。`); P(''); P('**🎯 行动建议**: 联名素材集中在 1-2 位强 IP KOL 上做"心智收口",而非分散给所有 KOL 各做一遍。本轮多个 KOL 都被要求带入联名,反而稀释了联名 IP 的可记忆度。'); P(''); P('### 7.4 「负向声音」可控但需要建立预警机制'); P(''); const neg = themeHits['负向/抱怨/差评'].items; P(`本期评论中负向相关 ${neg.length} 条,占比 ${(neg.length / allComments.length * 100).toFixed(2)}%,绝对值不高但内容多与 **"分量"、"排队等位"、"个别门店服务"** 相关。`); P(''); P('**🎯 行动建议**: 建立"评论关键词预警 SOP",对 "拉肚子 / 头发 / 苍蝇 / 食物中毒" 等高风险关键词触发即时介入,避免小红书/抖音差评发酵。'); P(''); // ===== 8. 投放结构问题 ===== P('## 八、投放结构性问题'); P(''); P('| 问题 | 数据支撑 | 建议 |'); P('|---|---|---|'); const lowEngagementCnt = items.filter((x) => num(x.engagementSubtotal || x.engagement) < 5).length; P(`| 长尾零互动占比偏高 | ${lowEngagementCnt}/${stats.total} 篇互动 <5 | 投后 24h 用算法实时筛选 Top 笔记追加薯条投流,放弃尾部 |`); const repeatedKols = items.filter((x) => x.contentType === '美食探店' && /上海|广东/.test(x.region || '')).length; P(`| 一二线探店笔记同质化 | ${repeatedKols} 篇集中"上海/广东 + 美食探店" | 部分内容设计差异化角度 (减脂餐/早午餐/家庭聚餐 场景化分赛道) |`); P(`| 区域分布不均 | Top 区域 ${topRegions[0][0]} ${topRegions[0][1].count} 篇 vs 长尾 | 选址扩张前先用 VOC 评论数据反向定位需求城市 |`); P('| 内容形态对比缺乏对照组 | 美食探店 220+ 篇缺少 A/B | 下一轮专设"鲜菜+套餐 vs 鲜菜单独"对照桶 |'); P(''); P('## 九、Next Steps'); P(''); P('| 优先级 | 行动 | 负责方 | 时间 |'); P('|---|---|---|---|'); P(`| P0 | 评论区运营 SOP:对 ${data.meta.with_api_comments} 条有评论笔记建立"鲜菜话术 + 求开店登记 + 风险词预警"三件套 | 品牌投放 | 本周 |`); P('| P0 | KOL 简报增加"鲜菜可视化"视觉指引,要求摘菜/凉拌/过水镜头 | 内容策略 | 下一轮立即 |'); P('| P1 | 「评论求开店地域榜」每月输出给拓店团队 | 数据团队 | 月度例会 |'); P('| P1 | 二创 KOC 池建立专属激励 (素材库 + 联名周边) | 投放执行 | 2 周内 |'); P('| P2 | 把本报告沉淀为内部知识库「品牌效果验证」第 5 案 (餐饮赛道首例 1k+ 评论 VOC) | 知识管理 | 报告冻结后 |'); P('| P2 | 抓取竞品对照 (陈香贵/张拉拉/马富/和府) Top 50 笔记评论 | 数据团队 | 2 周内 |'); P(''); P('---'); P(''); P('## 附录'); P(''); P('### A1 数据采集'); P(''); P('- `majiyong/scripts/collect-majiyong.js` - 解析 2 sheets + 短链 (87 直连 + 207 跳转) + 评论拉取 (3-5 页)'); P('- `majiyong/scripts/gen-majiyong-report.js` - 本报告生成器'); P('- 缓存目录: `majiyong/data/notes/` (293 文件), `majiyong/data/comments/` (293 文件)'); P(''); P('### A2 全量笔记表 (Top 60 by 评论数)'); P(''); P('| 标识 | 量级 | 类型 | 达人 | 区域 | 曝光 | 互动 | 评论 | API 抓取 | 笔记ID |'); P('|---|---|---|---|---|---:|---:|---:|---:|---|'); const top60 = [...items].sort((a, b) => num(b.comments) - num(a.comments)).slice(0, 60); for (const r of top60) { const tag = `${r.sheet === 'KOL合作表' ? 'L' : 'C'}-${r.seq}`; P(`| ${tag} | ${r.tier} | ${r.contentType} | ${safe(r.name)} | ${r.region} | ${fmt(r.impressionsSubtotal || r.impressions)} | ${fmt(r.engagementSubtotal || r.engagement)} | ${fmt(r.comments)} | ${(r._comments || []).length} | ${r.noteId || 'NULL'} |`); } P(''); P('---'); P(''); P('*报告基于可复用 VOC 工作流生成 · 共采集 ' + data.meta.total_api_comments + ' 条原始评论 (去重后 ' + allComments.length + ' 条,主+子评论) / 覆盖 ' + data.meta.with_api_comments + ' 篇笔记 · ' + new Date().toISOString().slice(0, 10) + '*'); fs.writeFileSync(OUT, lines.join('\n'), 'utf8'); console.log('✓ Generated:', OUT, '(' + lines.length + ' lines, ' + Math.round(Buffer.byteLength(lines.join('\n')) / 1024) + ' KB)');