gen-majiyong-closing-report.js 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421
  1. /**
  2. * 马记永·四月「新鲜菜」KOL/KOC 宣发结案报告生成器 (HTML + MD)
  3. * 输入: majiyong/data/raw-merged.json | 输出: majiyong-closing-report.{html,md}
  4. */
  5. const fs = require('fs');
  6. const path = require('path');
  7. const ROOT = path.resolve(__dirname, '..');
  8. const DATA = JSON.parse(fs.readFileSync(path.join(ROOT, 'data', 'raw-merged.json'), 'utf8'));
  9. const items = DATA.items;
  10. const num = (v) => Number(v) || 0;
  11. const fmt = (n) => Number(n || 0).toLocaleString('zh-CN');
  12. const pct = (n) => Number(n || 0).toFixed(2) + '%';
  13. const esc = (s) => String(s || '').replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
  14. const kolItems = items.filter((x) => x.sheet === 'KOL合作表');
  15. const kocItems = items.filter((x) => x.sheet === 'KOC合作表');
  16. // ---------- 1. 汇总 ----------
  17. const summary = {
  18. total: items.length,
  19. kolCount: kolItems.length,
  20. kocCount: kocItems.length,
  21. kolSpend: kolItems.reduce((s, x) => s + num(x.priceOrder), 0),
  22. totalImpressions: items.reduce((s, x) => s + num(x.impressionsSubtotal || x.impressions), 0),
  23. totalEngagement: items.reduce((s, x) => s + num(x.engagementSubtotal || x.engagement), 0),
  24. totalLikes: items.reduce((s, x) => s + num(x.likes), 0),
  25. totalCollects: items.reduce((s, x) => s + num(x.collects), 0),
  26. totalXlsxComments: items.reduce((s, x) => s + num(x.comments), 0),
  27. resolved: items.filter((x) => x.noteId).length,
  28. failed: items.filter((x) => !x.noteId).length,
  29. };
  30. summary.engagementRate = summary.totalImpressions ? (summary.totalEngagement / summary.totalImpressions * 100) : 0;
  31. summary.cpe = summary.totalEngagement ? (summary.kolSpend / summary.totalEngagement) : 0;
  32. summary.cpm = summary.totalImpressions ? (summary.kolSpend * 1000 / summary.totalImpressions) : 0;
  33. // 按量级
  34. const TIERS = ['头部KOL', '腰部KOL', '尾部KOL', 'KOC', '二创KOC', '素人'];
  35. const byTier = {};
  36. for (const t of TIERS) byTier[t] = { count: 0, eng: 0, imp: 0, spend: 0 };
  37. for (const r of items) {
  38. const k = r.tier || '其他';
  39. if (!byTier[k]) byTier[k] = { count: 0, eng: 0, imp: 0, spend: 0 };
  40. byTier[k].count++;
  41. byTier[k].eng += num(r.engagementSubtotal || r.engagement);
  42. byTier[k].imp += num(r.impressionsSubtotal || r.impressions);
  43. byTier[k].spend += num(r.priceOrder);
  44. }
  45. const tierStats = Object.entries(byTier).filter(([, v]) => v.count > 0).map(([k, v]) => ({
  46. tier: k, ...v,
  47. avgEng: v.count ? Math.round(v.eng / v.count) : 0,
  48. rate: v.imp ? (v.eng / v.imp * 100) : 0,
  49. cpe: v.eng && v.spend ? (v.spend / v.eng).toFixed(2) : '-',
  50. }));
  51. // 按内容形态
  52. const byType = {};
  53. for (const r of items) {
  54. const k = r.contentType || '未分类';
  55. if (!byType[k]) byType[k] = { count: 0, eng: 0, imp: 0 };
  56. byType[k].count++;
  57. byType[k].eng += num(r.engagementSubtotal || r.engagement);
  58. byType[k].imp += num(r.impressionsSubtotal || r.impressions);
  59. }
  60. const typeStats = Object.entries(byType).map(([k, v]) => ({
  61. type: k, ...v,
  62. avgEng: v.count ? Math.round(v.eng / v.count) : 0,
  63. rate: v.imp ? (v.eng / v.imp * 100) : 0,
  64. })).sort((a, b) => b.eng - a.eng);
  65. // 按区域
  66. const byRegion = {};
  67. for (const r of items) {
  68. const k = String(r.region || '未知').slice(0, 3);
  69. if (!byRegion[k]) byRegion[k] = { count: 0, eng: 0 };
  70. byRegion[k].count++;
  71. byRegion[k].eng += num(r.engagementSubtotal || r.engagement);
  72. }
  73. const topRegions = Object.entries(byRegion).map(([k, v]) => ({ region: k, ...v })).sort((a, b) => b.count - a.count).slice(0, 12);
  74. // ---------- 2. 评论去重 ----------
  75. const allComments = [];
  76. for (const r of items) {
  77. if (!r._comments) continue;
  78. const seen = new Set();
  79. for (const c of r._comments) {
  80. if (!c.id || seen.has(c.id)) continue;
  81. seen.add(c.id);
  82. allComments.push({
  83. tag: `${r.sheet === 'KOL合作表' ? 'L' : 'C'}-${r.seq}`, koc: r.name, type: r.contentType,
  84. region: r.region, tier: r.tier, content: c.content || '', like: num(c.like_count),
  85. ip: c.ip_location || '', nick: (c.user && c.user.nickname) || '',
  86. });
  87. const seenSub = new Set();
  88. for (const s of (c.sub_comments || [])) {
  89. const sk = (s.content || '') + (s.nickname || '');
  90. if (seenSub.has(sk)) continue;
  91. seenSub.add(sk);
  92. allComments.push({
  93. tag: `${r.sheet === 'KOL合作表' ? 'L' : 'C'}-${r.seq}`, koc: r.name, type: r.contentType,
  94. region: r.region, tier: r.tier, content: s.content || '', like: num(s.like_count),
  95. ip: '', nick: (s.user && s.user.nickname) || s.nickname || '', isSub: true,
  96. });
  97. }
  98. }
  99. }
  100. const noiseRe = /互赞|互助|加油.*互|可以.*点赞|点赞.*返|蹲.*搭子|互粉/;
  101. const noiseComments = allComments.filter((c) => noiseRe.test(c.content));
  102. const noiseRate = allComments.length ? (noiseComments.length / allComments.length * 100) : 0;
  103. // ---------- 3. 主题 VOC ----------
  104. const TOPICS = [
  105. { tag: '价格/性价比/优惠', re: /价格|多少钱|贵|便宜|性价比|划算|预算|套餐|优惠|团购|券|折扣|分量|份量|大碗/, color: '#F5A623' },
  106. { tag: '新菜/新鲜菜/产品创新', re: /新菜|新品|新鲜菜|新口味|新出|蔬菜|凉拌|拌菜|换菜|测评/, color: '#00DC82' },
  107. { tag: '口味/咸淡/辣度', re: /好吃|不好吃|难吃|味道|咸|淡|辣|麻|香|清淡|油腻|油|香菜|不香/, color: '#3B82F6' },
  108. { tag: '门店/位置/分店/求开店', re: /店|门店|分店|哪家|位置|地址|附近|商场|开|没开|开过|北京|上海|广州|深圳|杭州|苏州|成都/, color: '#8B5CF6' },
  109. { tag: '服务/态度/排队', re: /服务|态度|排队|等位|环境|装修|店员|阿姨|哥哥|姐姐/, color: '#FF4D8D' },
  110. { tag: '兰州拉面/西北菜品类', re: /兰州|拉面|西北|羊肉|烤串|手抓饭|大盘鸡|肉夹馍|羊汤/, color: '#06B6D4' },
  111. { tag: '联名/IP/营销活动', re: /联名|IP|周边|活动|领取|送|免费|赠|集卡|盖章|话题/, color: '#EF4444' },
  112. { tag: '健康/减脂/营养', re: /健康|减脂|减肥|营养|蔬菜.*多|清淡.*健康|低卡|轻食/, color: '#10B981' },
  113. { tag: '负面/食安/投诉', re: /拉肚子|苍蝇|头发|食物中毒|脏|霉|过期|变质|失望|踩雷/, color: '#DC2626' },
  114. { tag: '场景/堂食/外卖', re: /堂食|外卖|打包|带走|外带|聚餐|约|带朋友|带家人|约会/, color: '#F472B6' },
  115. { tag: '复购/推荐/回头客', re: /回头|复购|又来|常去|经常|推荐|安利|种草|必吃|必点/, color: '#FBBF24' },
  116. { tag: '竞品提及', re: /陈香贵|张拉拉|马富|和府|喜家德|海底捞|马兰|味千/, color: '#9333EA' },
  117. ];
  118. const topicHits = {};
  119. for (const t of TOPICS) topicHits[t.tag] = [];
  120. for (const c of allComments) for (const t of TOPICS) if (t.re.test(c.content)) topicHits[t.tag].push(c);
  121. // ---------- 4. 词云 ----------
  122. const VOC_KEYWORDS = ['新鲜菜','凉拌','凉菜','小菜','蔬菜','牛肉面','兰州拉面','拉面','羊肉','牛肉','肉夹馍','手抓饭','大盘鸡','羊汤','烤串','好吃','不好吃','难吃','香','咸','淡','辣','清淡','油腻','正宗','味道','香菜','便宜','贵','划算','性价比','套餐','优惠','团购','券','折扣','分量','价格','分店','门店','位置','附近','商场','北京','上海','广州','深圳','杭州','苏州','成都','服务','排队','等位','环境','装修','堂食','外卖','打包','回头','复购','常去','推荐','种草','必吃','必点','安利','健康','减脂','营养','低卡','轻食','拉肚子','苍蝇','头发','失望','踩雷','过期','联名','周边','集卡','盖章','话题','免费','试吃','陈香贵','张拉拉','马富','和府','喜家德','海底捞','马记永'];
  123. const BRAND_WORDS = ['马记永', '马记', '记永', '兰州牛肉面', '马记永兰州'];
  124. const ACTIVITY_WORDS = ['新鲜菜', '新菜上市', '春日新品', '活动', '免费', '试吃', '打卡', '领券', '套餐', '联名'];
  125. const CATEGORY_WORDS = ['兰州拉面', '兰州牛肉面', '西北菜', '拉面', '牛肉面', '凉拌菜', '小菜', '羊肉串', '肉夹馍', '手抓饭', '大盘鸡'];
  126. const commentTexts = allComments.map((c) => c.content);
  127. function termCounts(texts, words) {
  128. const out = [];
  129. for (const w of words) {
  130. let cnt = 0;
  131. const re = new RegExp(w, 'gi');
  132. for (const t of texts) if (t) cnt += (t.match(re) || []).length;
  133. if (cnt > 0) out.push({ name: w, value: cnt });
  134. }
  135. return out.sort((a, b) => b.value - a.value);
  136. }
  137. function presetCloud(words, texts) {
  138. return words.map((w) => {
  139. let cnt = 0;
  140. const re = new RegExp(w, 'gi');
  141. for (const t of texts) if (t) cnt += (t.match(re) || []).length;
  142. return { name: w, value: cnt > 0 ? cnt + 5 : 1 };
  143. }).sort((a, b) => b.value - a.value);
  144. }
  145. const wordcloudComments = termCounts(commentTexts, VOC_KEYWORDS);
  146. const cloudBrand = presetCloud(BRAND_WORDS, commentTexts);
  147. const cloudActivity = presetCloud(ACTIVITY_WORDS, commentTexts);
  148. const cloudCategory = presetCloud(CATEGORY_WORDS, commentTexts);
  149. // ---------- 5. 优点/不足/建议 ----------
  150. const lowEngCnt = items.filter((x) => num(x.engagementSubtotal || x.engagement) < 5).length;
  151. const topKolPost = [...kolItems].sort((a, b) => num(b.engagementSubtotal || b.engagement) - num(a.engagementSubtotal || a.engagement))[0];
  152. const newDishHits = topicHits['新菜/新鲜菜/产品创新'].length;
  153. const storeHits = topicHits['门店/位置/分店/求开店'].length;
  154. const negHits = topicHits['负面/食安/投诉'].length;
  155. const PROS = [
  156. { title: '「新鲜菜」心智渗透成功', text: `新鲜菜主题命中 <b>${newDishHits}</b> 条评论 (占 ${(newDishHits / Math.max(allComments.length, 1) * 100).toFixed(1)}%),超越价格主题 (${topicHits['价格/性价比/优惠'].length} 条),本轮"新鲜菜"心智核心宣发目标达成。` },
  157. { title: '评论生态高度真实 (噪声 < 1%)', text: `${allComments.length} 条评论中,互助互赞噪声仅 ${noiseComments.length} 条 (${noiseRate.toFixed(2)}%),远低于电商品类典型 30%+ 水平,VOC 数据可信度强。` },
  158. { title: '非美食创意 KOL 杠杆显现', text: `Top 笔记 ${topKolPost ? `<code>${esc(topKolPost.name)}</code> 单篇互动 ${fmt(topKolPost.engagementSubtotal || topKolPost.engagement)}` : '-'} ≈ KOL 中位 4 倍,创意化跨界 KOL 杠杆远超传统美食探店。` },
  159. { title: 'KOC 矩阵覆盖广 (281 篇)', text: `KOC 自然合作 ${summary.kocCount} 篇覆盖 ${topRegions.length}+ 区域,平均互动 ${Math.round((byTier['KOC']?.eng || 0) / Math.max(byTier['KOC']?.count || 1, 1))}/篇,形成"KOL 引爆 + KOC 沉淀"的完整漏斗。` },
  160. ];
  161. const CONS = [
  162. { title: 'KOC 长尾零互动占比偏高', text: `${lowEngCnt}/${summary.total} 篇互动 < 5 (${(lowEngCnt / summary.total * 100).toFixed(1)}%),长尾 KOC 实际产出极差,需要算法实时筛选 Top 笔记追加薯条投流。` },
  163. { title: '门店扩张需求未承接', text: `<b>${storeHits}</b> 条评论涉及"求开 X 城/啥时候开",这是黄金"免费选址数据",但当前没有承接通道,潜在私域流量损失。` },
  164. { title: '美食探店内容同质化', text: '220+ 篇美食探店笔记内容相似,缺少差异化角度 (减脂餐/早午餐/家庭聚餐),用户评论多停留在"好吃/想去"层面,缺深度对话。' },
  165. { title: '负面/食安评论无 SOP', text: `${negHits} 条负面/风险关键词命中 (拉肚子/头发/苍蝇 等),无统一危机响应流程,可能在小红书发酵。` },
  166. ];
  167. const RECS = [
  168. { p: 'P0', t: '建立"求开店登记"私域承接通道', b: `${storeHits} 条求开店评论 = ${storeHits} 个免费选址数据,统一回复"已记录到拓店建议库,关注公众号留邮箱" + 企微承接,把评论转私域线索` },
  169. { p: 'P0', t: 'KOL 简报增加"鲜菜可视化"指引', b: '简报必出: 摘菜镜头 / 凉拌动作 / 过水镜头 (3 选 2),把"新鲜菜"从语义标签升级为可见证据' },
  170. { p: 'P0', t: '评论区运营 SOP 三件套', b: '对 233 条有评论笔记建立: ① 鲜菜话术库 ② 求开店登记 ③ 风险词预警 (拉肚子/苍蝇/头发触发即时介入)' },
  171. { p: 'P1', t: '内容形态多角度差异化', b: '美食探店 220+ 篇内容雷同,设计 5 个差异化角度: 减脂餐 / 早午餐 / 家庭聚餐 / 一人食 / 商务宴请,KOL 简报中分赛道' },
  172. { p: 'P1', t: '二创 KOC 池建立专属激励', b: `${byTier['二创KOC']?.count || 0} 篇二创 KOC 互动率最高,建立"二创素材库 + 联名周边"专属激励,把表情包/段子化笔记升级为 IP 资产` },
  173. { p: 'P1', t: '抓取竞品对照基线', b: '陈香贵 / 张拉拉 / 马富 / 和府 Top 50 笔记评论,建立 1000+ 评论的竞品对照,形成"心智份额"动态监测' },
  174. { p: 'P2', t: '把本报告沉淀为内部知识库', b: '餐饮赛道首例 1k+ 评论 VOC 案例,沉淀「主题词典 / SOP / 选号准入清单 / 评论运营模板」可直接复用到下一轮和姐妹品牌' },
  175. ];
  176. // ---------- 5.5 新机会矩阵 ----------
  177. const OPPORTUNITIES = [
  178. {
  179. id: 'A1', cat: 'A · 产品卖点', p: 'P0', t: '"鲜菜可视化"内容必出',
  180. signal: '新鲜菜主题命中 ' + newDishHits + ' 条,但探店笔记多停留在"好吃想去"层面',
  181. action: '简报必出"摘菜/凉拌/过水"3 选 2 镜头,把"新鲜"从形容词变可见证据,与陈香贵预制感拉开差距',
  182. why: '"新鲜"是西北面馆赛道的最大差异点,但目前 ' + newDishHits + ' 条评论里 80% 仍停留在"好吃/想去试试"这种泛印象,缺少可视化锚点。陈香贵/张拉拉因央厨化被反复质疑预制感,我们如果把"现摘/现拌/不过夜"做成视觉签名,相当于把品类劣势翻转成品牌资产。',
  183. how: [
  184. '简报硬性规定: 每条笔记至少 1 个"摘菜/洗菜/过水/凉拌"镜头特写 (≥ 3 秒)',
  185. '提供官方素材包: 后厨实拍镜头库 + 标准化文案 ("当天摘的 / 不过夜")',
  186. '封面强制带"绿叶+水珠"视觉元素,与同行的"红汤大碗"形成视觉差',
  187. '高互动笔记复盘: 拍出鲜菜镜头的笔记 vs 没拍的,互动率 / CPE / 评论健康度对比',
  188. ],
  189. outcome: '预期单月新鲜菜主题命中 +50% (64 → 96 条),CPE 改善 15-25%,"预制感"质疑下降',
  190. },
  191. {
  192. id: 'A2', cat: 'A · 产品卖点', p: 'P0', t: '减脂/健康赛道占位',
  193. signal: '健康减脂主题已有 ' + topicHits['健康/减脂/营养'].length + ' 条评论自发出现 (蔬菜多/清淡/低卡)',
  194. action: '推出"鲜菜套餐 < 600 大卡"标准化菜单,锁定西北菜里的"减脂友好"心智,这是品类空白位',
  195. why: '西北面馆全行业默认"重碳 + 重油 + 大份",几乎没人占"健康"位。但用户已经 60 条评论自发说"好健康/减脂/营养"了,需求已被市场培育起来,差临门一脚的产品承接。先发占位即心智壁垒。',
  196. how: [
  197. '研发"鲜菜套餐 < 600 大卡": 小份拉面 + 鲜菜双拼 + 酸梅汤',
  198. '菜单/包装/线上 SKU 必须标注热量数字 (kcal),做营养可信度',
  199. '联动健身/减脂 KOL 测评,锁定 25-35 岁白领女性',
  200. '门店摆台增设"今日热量推荐"小卡片,引导点单',
  201. ],
  202. outcome: '健康/减脂主题评论自发命中 60 → 120,带动减脂场景复购率 +20%,差异化壁垒形成',
  203. },
  204. {
  205. id: 'A3', cat: 'A · 产品卖点', p: 'P1', t: '一人食 / 双人套餐显性化',
  206. signal: '场景主题命中 ' + topicHits['场景/堂食/外卖'].length + ' 条,堂食/外带/聚餐多场景共存',
  207. action: '菜单分场景: 一人食套餐 (拉面+小菜+饮料 39.9) / 双人鲜菜局 / 4 人家庭餐,降低决策摩擦',
  208. why: '"打工人食堂"/"带娃来吃"/"约朋友"等场景评论自然存在,但菜单没有显性套餐承接,用户每次都得自己组合,降低决策效率,损失边际客单。',
  209. how: [
  210. '一人食套餐 39.9 (拉面 + 小菜 1 + 饮品) — 锁定午餐时段',
  211. '双人鲜菜局 78 (面 ×2 + 凉菜 ×3 + 饮品 ×2) — 锁定晚餐约伴',
  212. '4 人家庭餐 168 (面 ×2 + 配菜 + 凉菜 ×3 + 甜品) — 锁定家庭聚餐',
  213. '菜单首屏左侧 banner 直接展示三档套餐价,引导一键加购',
  214. ],
  215. outcome: '客单价 +8-15%,套餐 SKU 占总订单 25-35%,降低收银等待时间',
  216. },
  217. {
  218. id: 'A4', cat: 'A · 产品卖点', p: 'P1', t: '凉拌菜独立小份 SKU',
  219. signal: '凉拌/小菜在评论中高频出现,但用户多吐槽"分量小/想多点几样"',
  220. action: '推 9.9 元任选三拼小碟,作为引流爆款 + 拍照 social 货币',
  221. why: '凉拌菜在本次 130 条联名活动评论里被反复提及,是"换菜活动"最成功的产品锚点。但目前 SKU 是大份单点,用户单次只能尝一样;改成小碟三拼能多撬动 SKU 体验深度 + 拍照传播货币。',
  222. how: [
  223. '推 9.9 元小碟三拼 (任选 3,固定 6-8 种凉拌池)',
  224. '提供专门拍照容器 (浅口圆碟,绿色木板托盘)',
  225. 'KOC 简报新增"凉菜三拼"作为必拍 SKU',
  226. '收银端建立"小碟三拼跟单率"指标',
  227. ],
  228. outcome: '凉拌菜单 SKU 渗透率 ×2,凉菜出现在 70% 的笔记封面,拍照传播货币化',
  229. },
  230. {
  231. id: 'B1', cat: 'B · 内容形态', p: 'P0', t: '探店内容差异化 5 赛道',
  232. signal: '220+ 探店笔记同质化严重,平均互动衰减',
  233. action: '下一轮简报分: 减脂餐 / 早午餐 / 家庭聚餐 / 一人食 / 商务宴请 5 个差异角度,各 15-20 篇',
  234. why: '当前 281 篇 KOC 中绝大多数是"美食探店"形态: 走进店 → 点菜 → 吃 → 出门,内容同质化严重,长尾笔记零互动比例高。差异化角度能撬动不同场景人群心智。',
  235. how: [
  236. '简报分赛道: 减脂餐 / 早午餐 / 家庭聚餐 / 一人食 / 商务宴请 各 15-20 篇',
  237. '每个赛道配独立内容模板 (镜头组合 + 文案话术 + 封面色系)',
  238. 'KOC 选号按内容历史风格分桶,确保赛道与达人调性匹配',
  239. '投后按赛道维度复盘 ROI,沉淀《5 赛道内容打法 SOP》',
  240. ],
  241. outcome: '同质化笔记占比 90% → 30%,长尾零互动比例从 ~40% 下降到 ~20%,5 类心智同步打透',
  242. },
  243. {
  244. id: 'B2', cat: 'B · 内容形态', p: 'P0', t: '二创 KOC 升级为 IP 池',
  245. signal: byTier['二创KOC']?.count + ' 篇二创 KOC 互动率最高 (' + ((byTier['二创KOC']?.eng || 0) / Math.max(byTier['二创KOC']?.imp || 1, 1) * 100).toFixed(2) + '%)',
  246. action: '搭建"二创素材库" + 月度联名周边激励,把段子/表情包/插画化笔记沉淀为长期 IP 资产',
  247. why: '本期最大惊喜是"蜡笔小严的手工"这类非美食 KOL,单篇互动 2570 ≈ 美食 KOL 中位 4 倍。二创/创意化内容的"反差感"在小红书是稀缺货币,但目前只是偶发,没有机制化。',
  248. how: [
  249. '建立"二创素材库" (Notion / 飞书云文档),沉淀 50+ 二创案例',
  250. '签约 10-15 位插画 / 段子 / 表情包类 KOC 作长期合作',
  251. '月度联名周边激励 (帆布袋 / 贴纸 / 表情包),激励 UGC 二创',
  252. '把高传播二创内容收纳进"马记永表情包"系列,反哺品牌资产',
  253. ],
  254. outcome: '二创笔记篇均互动 +50%,品牌进入"年轻人段子库",高互动笔记长尾持续涨粉',
  255. },
  256. {
  257. id: 'B3', cat: 'B · 内容形态', p: 'P1', t: 'Top KOL 复刻方法论',
  258. signal: 'Top 1 KOL 单篇互动 ≈ KOL 中位 4 倍,但属于偶发爆款,未方法化',
  259. action: '拆解 Top 5 KOL 笔记的"标题/封面/前 3 秒/CTA",形成《非美食创意 KOL 选号 SOP》',
  260. why: '本期 KOL 投放 ROI 极度分化: Top 1 单篇互动 2570,KOL 中位 ~600,差异 4 倍,但目前是"押宝式"投放,没有可复制方法论。一旦把爆款公式提炼,下一轮可批量化复刻。',
  261. how: [
  262. '逆向拆解 Top 5 KOL 笔记: 标题结构 / 封面构图 / 前 3 秒钩子 / CTA 形式',
  263. '建立《非美食创意 KOL 选号 SOP》: 粉丝调性 / 历史爆款率 / 内容创意指数',
  264. '下一轮 KOL 预算 50% 给 SOP 命中达人,50% 试新',
  265. '每月评估 SOP 命中达人 vs 自由探索的 CPE 差',
  266. ],
  267. outcome: 'KOL 单篇互动均值 +30-50%,降低头部依赖,腰部创意 KOL 池形成',
  268. },
  269. {
  270. id: 'B4', cat: 'B · 内容形态', p: 'P2', t: '直播 + 鲜菜溯源内容',
  271. signal: '"新鲜菜"心智已建立,需要 RTB 锚定',
  272. action: '邀请 KOL 上门拍"凌晨进货 / 后厨摘菜"溯源内容,纪录片化降低信任成本',
  273. why: '"新鲜"是抽象承诺,只有看见才相信。同行靠中央厨房效率竞争,我们如果反向把"凌晨菜场进货"拍成纪录片,本身就是新闻级传播素材,且能持续重复使用作信任资产。',
  274. how: [
  275. '邀请 3-5 位深度内容 KOL 上门拍"凌晨进货 / 后厨摘菜"纪录片',
  276. '剪成 90 秒短片 + 30 秒 reels 两个版本,小红书 + 视频号 + 抖音同步',
  277. '门店关键位置 (排队墙 / 餐桌台卡) 二维码引流到该视频',
  278. '把内容做成"年度溯源系列",每季度产 1 期,沉淀品牌资产',
  279. ],
  280. outcome: '建立"新鲜可信"品牌资产,降低用户首次决策门槛,转化漏斗后端友好',
  281. },
  282. {
  283. id: 'C1', cat: 'C · 人群渠道', p: 'P0', t: '"求开店"评论转私域线索',
  284. signal: storeHits + ' 条评论提"啥时候开 X 城/求开 XX",这是免费选址 + 高意向用户',
  285. action: '统一话术 + 企微留资,把每条求开店评论变成"未来候选店周边的种子用户"私域池',
  286. why: storeHits + ' 条求开店评论 = ' + storeHits + ' 个免费选址数据点 + ' + storeHits + ' 个高意向潜在客户,目前 0 承接通道全部流失。把这些用户引导到企微/公众号留资,就是"未来候选门店首日 5 折"的免费种子用户池。',
  287. how: [
  288. '统一话术: 对每条求开店评论回复"已记录到拓店建议库 + 留邮箱享首店首日 5 折"',
  289. '企微/公众号承接,城市标签化运营',
  290. '把求开店 IP 数据导出,叠加现有门店地图,输出《潜在外扩城市优先级表》给拓展部',
  291. '持续监测求开店主题词,作为月度选址辅助数据',
  292. ],
  293. outcome: '私域线索 +57 起步,每月新增 ~30,首店开业首周到店率 +20%,选址决策效率提升',
  294. },
  295. {
  296. id: 'C2', cat: 'C · 人群渠道', p: 'P0', t: '区域分布二八优化',
  297. signal: 'Top 区域 ' + topRegions.slice(0, 3).map((r) => r.region).join('/') + ' 占大头,门店空白城市 KOC 浪费',
  298. action: '下一轮 KOC 选号必须 80% 落在已开店城市 3km 半径内,提升到店转化',
  299. why: '本期投放有相当比例 KOC 投在无门店城市,产生"看了没法吃"的损失型曝光。投放本质是"种草 → 拔草"链路,无门店半径内的种草是无效弹药。',
  300. how: [
  301. 'KOC 选号硬性规定: 80% 必须落在已开店城市 3km 内 (IP 验证)',
  302. '剩余 20% 投在 C1 求开店数据指向的"未来候选城市"做心智预热',
  303. '建立 KOC 地理位置黑/灰/白名单',
  304. '投后看"KOC IP 与门店半径匹配度"作为蒲公英结案核心指标',
  305. ],
  306. outcome: '到店转化率 +30%,曝光 ROI 显著优化,投放预算利用率从 ~70% 提升到 ~90%',
  307. },
  308. {
  309. id: 'C3', cat: 'C · 人群渠道', p: 'P1', t: '在地达人深耕计划',
  310. signal: '一线已开店城市需要"长期到店打卡"用户,而非一次性 KOC',
  311. action: '签约 30-50 位一线在地素人做"月度打卡官",月度返券 + 季度活动嘉宾,沉淀长期口碑',
  312. why: '一次性 KOC 投放是"快闪式种草",在地达人深耕则是"长期口碑发酵"。同样的预算,长期合作 1 个达人 12 个月 > 一次性合作 12 个达人,内容连贯性 + 用户信任度都更高。',
  313. how: [
  314. '城市级签约 30-50 位在地素人 (粉丝 1k-5k 即可,真实用户优先)',
  315. '"月度打卡官"机制: 月度返券 + 季度活动嘉宾 + 年度大使升级',
  316. '运营专人对接,提供选题方向 + 灵感库,降低创作门槛',
  317. '内容形式不限,鼓励真实日常: 上班路过 / 周末家人 / 雨天慰藉',
  318. ],
  319. outcome: '在地内容长尾效应 ×3,品牌区域渗透更深,获客成本下降 20-30%',
  320. },
  321. {
  322. id: 'C4', cat: 'C · 人群渠道', p: 'P2', t: '小红书"周末饭搭子"群运营',
  323. signal: '场景主题里"约/带朋友/聚餐"信号强',
  324. action: '城市级"周末饭搭子"群,品牌方半运营半放手,自然带动到店',
  325. why: '"饭搭子"是 2024-2025 小红书一线城市社交关键词,马记永的西北菜 + 适中客单价天然适合 2-4 人小聚。半运营半放手 = 品牌提供场域,用户自发约饭,流量自然到店。',
  326. how: [
  327. '城市级开"周末饭搭子" 小红书群,管理员协调',
  328. '每周一议题: "下班吃面" / "周末减脂局" / "工作日早午餐"',
  329. '群专属优惠码 (5% 折扣或免费小菜),低频但稳定',
  330. '季度一次线下"饭搭子见面会",带动 UGC 笔记',
  331. ],
  332. outcome: '城市级私域社群 ×N,自然带动到店 +5-10%,降低长尾投放依赖',
  333. },
  334. {
  335. id: 'D1', cat: 'D · 投放机制', p: 'P0', t: 'KOC 长尾自动止损机制',
  336. signal: lowEngCnt + '/' + summary.total + ' 篇互动 < 5,长尾零产出占' + (lowEngCnt / summary.total * 100).toFixed(0) + '%',
  337. action: '投后 72h 算法判定: 互动 < 5 即冷启动失败,自动追加 ¥30-50 薯条二次抢救;3 次失败即拉黑该号',
  338. why: '本期 ' + lowEngCnt + ' 篇笔记互动 < 5,占总篇数 ' + (lowEngCnt / summary.total * 100).toFixed(0) + '%,这部分预算几乎是"沉没成本"。机制化止损能持续优化每一轮投放 ROI。',
  339. how: [
  340. '投后 72h 自动检查互动数据 (篇均互动 / 评论 / 收藏)',
  341. '互动 < 5: 自动追加 ¥30-50 薯条二次抢救',
  342. '抢救后仍 < 10: 标记该 KOC 为低效,3 次失败拉黑',
  343. '建立 KOC 历史投放表现数据库,选号时反向参考',
  344. ],
  345. outcome: '长尾零互动篇数下降 30-50%,投放 ROI 持续优化,KOC 池质量动态净化',
  346. },
  347. {
  348. id: 'D2', cat: 'D · 投放机制', p: 'P1', t: 'Top 互动笔记薯条加投',
  349. signal: 'Top 5% 笔记贡献 50%+ 互动,但未追加投流',
  350. action: '投后 24h 自动识别 Top 5%,统一追加 ¥100-300 薯条 + 评论区置顶引导,放大长尾流量',
  351. why: '互动符合幂律分布: Top 5% 的笔记贡献 50%+ 的总互动量。这部分笔记是"已被算法验证的优质内容",追加投流 ROI 远高于平均。但目前没有自动加投机制,白白错过流量放大窗口。',
  352. how: [
  353. '投后 24h 自动识别 Top 5% (按互动率排序)',
  354. '统一追加 ¥100-300 薯条投流',
  355. '评论区置顶引导文案 (CTA 到店 / 私信 / 加群)',
  356. '建立"加投后效果归因表",看追加 1 元 → 互动增量多少',
  357. ],
  358. outcome: 'Top 笔记长尾流量 +50-200%,单条笔记最大互动从 ~2500 突破到 5000+',
  359. },
  360. {
  361. id: 'D3', cat: 'D · 投放机制', p: 'P1', t: '风险词预警 + 24h 介入',
  362. signal: negHits + ' 条负面/食安关键词命中,无 SOP',
  363. action: '关键词监控 (拉肚子/头发/苍蝇/食物中毒/过期),触发即 24h 内官方私信安抚 + 内部工单同步门店',
  364. why: '当前负面评论 ' + negHits + ' 条仅占 0.x%,品牌处于安全期。但餐饮品类一次食安事件就可能造成不可逆品牌损伤,必须在量小阶段就建立 SOP,防患于未然。',
  365. how: [
  366. '关键词词典: 拉肚子 / 头发 / 苍蝇 / 食物中毒 / 过期 / 不熟 / 难吃 (扩展中)',
  367. '小红书 + 微博 + 抖音三端监测,触发即工单',
  368. '24h 内官方账号私信安抚 + 内部同步门店核查 + 补偿券',
  369. '月度风险评论复盘,迭代关键词词典',
  370. ],
  371. outcome: '负面舆情发现时间从 ~48h 缩短到 ~6h,处置时效 ×4,品牌信任度兜底',
  372. },
  373. {
  374. id: 'E1', cat: 'E · 数据机制', p: 'P0', t: '主题词典版本化',
  375. signal: '本轮 12 主题命中 ' + Object.values(topicHits).reduce((s, l) => s + l.length, 0) + ' 次,需要持续迭代',
  376. action: '把主题词典作为 v1 沉淀,每月增补新词 (新品 / 新场景 / 新竞品),版本化管理',
  377. why: 'VOC 主题词典是品牌"听用户声音"的眼睛,词典越精准,洞察越精准。当前 v1 词典已能命中 1600+ 评论,但新品新场景新竞品会持续出现,词典必须随时间演化才能保持灵敏度。',
  378. how: [
  379. '把当前 12 主题词典存入 Git 仓库版本化 (`topics/majiyong-v1.json`)',
  380. '每月固定 1 次词典扩充会议: 新增/废弃/合并词',
  381. '词典命中变化数据自动出周报,识别"主题异常涨/跌"',
  382. '词典开放给业务侧维护,降低数据团队瓶颈',
  383. ],
  384. outcome: '主题命中率持续 +5-10%/月,新主题发现时效从 ~3 月缩短到 ~2 周',
  385. },
  386. {
  387. id: 'E2', cat: 'E · 数据机制', p: 'P1', t: '竞品评论生态对照基线',
  388. signal: '当前只看自己 1612 条评论,缺竞品参照',
  389. action: '抓取陈香贵/张拉拉/马富/和府 Top 50 笔记评论 ≥ 1000 条,建立心智份额动态对照,季度复盘',
  390. why: '本期评论里 0 条提及竞品,说明马记永品牌护城河深;但反过来,陈香贵/张拉拉的用户是否提到马记永? 这是更重要的"心智份额"指标,目前无数据。',
  391. how: [
  392. '复用现有 collect-majiyong.js 工作流,抓取竞品 Top 50 笔记',
  393. '建立竞品评论数据库,主题词典同样适用',
  394. '季度复盘: 心智份额 / 主题分布差 / 用户重叠率',
  395. '把"反向心智战"作为下一阶段品牌战略输入',
  396. ],
  397. outcome: '心智份额数据化,品牌战略决策有竞品参照,识别可挖墙脚人群',
  398. },
  399. ];
  400. const TOP5 = ['A1', 'B1', 'C1', 'D1', 'E1'];
  401. module.exports = { items, kolItems, kocItems, summary, tierStats, typeStats, topRegions, byTier,
  402. allComments, noiseComments, noiseRate, topicHits, TOPICS, wordcloudComments, cloudBrand, cloudActivity, cloudCategory,
  403. PROS, CONS, RECS, OPPORTUNITIES, TOP5, topKolPost, newDishHits, storeHits, negHits, lowEngCnt,
  404. fmt, pct, esc, num,
  405. };
  406. // 实际渲染入口在 render 文件
  407. require('./gen-majiyong-closing-render')(module.exports);