| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427 |
- #!/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)');
|