#!/usr/bin/env node
/**
* 洪城到家 · VOC 洞察报告 v4 生成器 (优化版)
* 基于 v3 模板,集成真实 XHS + Douyin + Drama 数据
*
* 核心理念(报告方面.md):
* 1. 抄作业 - 学习头部品牌打法
* 2. 短剧破圈 - 借助短剧内容营销
* 3. VOC新机会 - 从真实用户声音发现
*
* 数据来源:
* - XHS: docs/洪城到家/raw/xhs/*.json (6个品牌)
* - Douyin: docs/洪城到家/raw/douyin/*.json (23个关键词)
* - Drama: docs/洪城到家/raw/douyin/drama/*.json (4个短剧关键词)
*
* 用法:
* node scripts/tools/hongcheng-render-v4.js
*/
const fs = require('fs');
const path = require('path');
const ROOT = path.resolve(__dirname, '..', '..');
const XHS_DIR = path.join(ROOT, 'docs', '洪城到家', 'raw', 'xhs');
const DOUYIN_DIR = path.join(ROOT, 'docs', '洪城到家', 'raw', 'douyin');
const DRAMA_DIR = path.join(DOUYIN_DIR, 'drama');
const OUT_FILE = path.join(ROOT, 'reports', 'hongcheng-voc-insight-report-v4.html');
function esc(s) { return String(s ?? '').replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])); }
function fmt(n) { if (n == null) return '0'; if (n >= 10000) return (n/10000).toFixed(1).replace(/\.0$/,'')+'w'; if (n >= 1000) return (n/1000).toFixed(1).replace(/\.0$/,'')+'k'; return String(n); }
function truncate(s, n=160) { s = String(s ?? ''); return s.length > n ? s.slice(0,n-1)+'…' : s; }
const HYPOTHESES = {
H1: { title: '医院地推', desc: '直接触达即将生产的精准用户', color: '#F5A623' },
H2: { title: '价格透明', desc: '用户对价格不透明是核心痛点', color: '#00DC82' },
H3: { title: '短剧营销', desc: '借南昌万亿短剧政策红利', color: '#8B5CF6' },
H4: { title: '专业度信任', desc: '用户判断月嫂专业度的核心信号', color: '#FF4D8D' },
H5: { title: '社区店威胁', desc: '社区月嫂门店主要威胁是"价格低"', color: '#3B82F6' },
H6: { title: '老带新', desc: '老客户转介绍是最高效获客方式', color: '#EF4444' },
H7: { title: '搜索主力', desc: '小红书是搜索承接主力平台', color: '#F97316' },
H8: { title: '服务保障', desc: '"不满意能换"能显著降低决策门槛', color: '#6B6B6B' },
};
// ============ 数据加载 ============
function loadXHSData() {
if (!fs.existsSync(XHS_DIR)) return { notes: [], comments: [], byBrand: {}, stats: { notes: 0, comments: 0, likes: 0 } };
const files = fs.readdirSync(XHS_DIR).filter(f => f.endsWith('.json'));
const all = { notes: [], comments: [], byBrand: {}, stats: { notes: 0, comments: 0, likes: 0 } };
for (const file of files) {
const brand = file.replace('.json', '').replace('月嫂', '');
const raw = JSON.parse(fs.readFileSync(path.join(XHS_DIR, file), 'utf8'));
all.byBrand[brand] = raw;
for (const note of (raw.top_notes || [])) {
note.brand = brand;
note.platform = 'xhs';
all.notes.push(note);
all.stats.notes++;
all.stats.likes += note.liked_count || 0;
for (const c of (note.comments || [])) {
c.note_id = note.note_id;
c.brand = brand;
c.platform = 'xhs-comment';
all.comments.push(c);
all.stats.comments++;
}
}
}
return all;
}
function loadDouyinData() {
if (!fs.existsSync(DOUYIN_DIR)) return { videos: [], byKeyword: {}, stats: { videos: 0, likes: 0, comments: 0 } };
const files = fs.readdirSync(DOUYIN_DIR).filter(f => f.endsWith('.json') && f !== 'audit.log');
const all = { videos: [], byKeyword: {}, stats: { videos: 0, likes: 0, comments: 0 } };
for (const file of files) {
const keyword = file.replace('.json', '');
const raw = JSON.parse(fs.readFileSync(path.join(DOUYIN_DIR, file), 'utf8'));
all.byKeyword[keyword] = raw;
for (const v of (raw.top_videos || [])) {
v.keyword = keyword;
v.platform = 'douyin';
all.videos.push(v);
all.stats.videos++;
all.stats.likes += v.statistics?.digg_count || 0;
all.stats.comments += v.statistics?.comment_count || 0;
}
}
return all;
}
function loadDramaData() {
if (!fs.existsSync(DRAMA_DIR)) return { dramas: [], stats: { videos: 0, likes: 0, topLikes: 0, comments: 0 } };
const files = fs.readdirSync(DRAMA_DIR).filter(f => f.endsWith('.json'));
const all = { dramas: [], stats: { videos: 0, likes: 0, topLikes: 0, comments: 0 } };
for (const file of files) {
const raw = JSON.parse(fs.readFileSync(path.join(DRAMA_DIR, file), 'utf8'));
for (const v of (raw.top_videos || [])) {
v.dramaKeyword = file.replace('.json', '');
v.platform = 'drama';
all.dramas.push(v);
all.stats.videos++;
const likes = v.statistics?.digg_count || 0;
const comments = v.statistics?.comment_count || 0;
all.stats.likes += likes;
all.stats.comments += comments;
if (likes > all.stats.topLikes) all.stats.topLikes = likes;
}
}
return all;
}
// ============ 数据分析 ============
function analyzeXHS(xhs) {
const { notes, comments, byBrand, stats } = xhs;
const sentiment = { positive: 0, negative: 0, neutral: 0, conflicted: 0 };
const hypothesisCoverage = { H1: 0, H2: 0, H3: 0, H4: 0, H5: 0, H6: 0, H7: 0, H8: 0 };
for (const n of notes) {
sentiment[n.sentiment || 'neutral']++;
if (n.hypotheses && Array.isArray(n.hypotheses)) {
for (const h of n.hypotheses) if (hypothesisCoverage[h]) hypothesisCoverage[h]++;
}
}
for (const c of comments) {
sentiment[c.sentiment || 'neutral']++;
if (c.hypotheses && Array.isArray(c.hypotheses)) {
for (const h of c.hypotheses) if (hypothesisCoverage[h]) hypothesisCoverage[h]++;
}
}
const tagStats = {};
for (const n of notes) for (const t of (n.inferred_tags || [])) tagStats[t] = (tagStats[t] || 0) + 1;
const topTags = Object.entries(tagStats).sort((a, b) => b[1] - a[1]).slice(0, 15);
const brandStats = {};
for (const [brand, data] of Object.entries(byBrand)) {
brandStats[brand] = {
notes: data.notes_processed || 0,
comments: data.total_comments || 0,
likes: (data.top_notes || []).reduce((s, n) => s + (n.liked_count || 0), 0)
};
}
const topNotes = [...notes].sort((a, b) => (b.liked_count || 0) - (a.liked_count || 0)).slice(0, 6);
const topComments = [...comments].sort((a, b) => (b.comment_likes || 0) - (a.comment_likes || 0)).slice(0, 4);
return { stats, sentiment, hypothesisCoverage, topTags, topNotes, topComments, brandStats, totalNotes: notes.length, totalComments: comments.length };
}
function analyzeDouyin(dy) {
const { videos, byKeyword, stats } = dy;
const kwStats = {};
for (const [kw, data] of Object.entries(byKeyword)) {
kwStats[kw] = {
videos: data.total_videos_found || 0,
likes: (data.top_videos || []).reduce((s, v) => s + (v.statistics?.digg_count || 0), 0),
topLikes: (data.top_videos || [])[0]?.statistics?.digg_count || 0
};
}
const topVideos = [...videos].sort((a, b) => (b.statistics?.digg_count || 0) - (a.statistics?.digg_count || 0)).slice(0, 6);
return { stats, kwStats, topVideos, totalVideos: videos.length };
}
function analyzeDrama(drama) {
const { dramas, stats } = drama;
const topVideos = [...dramas].sort((a, b) => (b.statistics?.digg_count || 0) - (a.statistics?.digg_count || 0)).slice(0, 6);
const genreStats = {};
for (const d of dramas) {
const kw = d.dramaKeyword || '其他';
genreStats[kw] = (genreStats[kw] || 0) + 1;
}
return { stats, topVideos, totalDramas: dramas.length, genreStats };
}
// ============ 组件 ============
function vocCard(item, type = 'note') {
const senColors = { positive: '#00DC82', negative: '#EF4444', neutral: '#6B6B6B', conflicted: '#F97316' };
const platformColors = { 'xhs': '#FF2442', 'xhs-note': '#FF2442', 'xhs-comment': '#FF6B6B', 'drama': '#8B5CF6', 'douyin': '#1A1A1A' };
const platformIcons = { 'xhs': '红', 'xhs-note': '红', 'xhs-comment': '评', 'drama': '剧', 'douyin': '抖' };
let content, likes, author, sentiment, tags, ip;
if (type === 'xhs-note' || type === 'xhs') {
content = item.content || item.title || '';
likes = item.liked_count || 0;
author = item.author || item.user_nickname || '匿名';
sentiment = item.sentiment || 'neutral';
tags = item.inferred_tags || [];
ip = item.ip_location || '';
} else if (type === 'xhs-comment') {
content = item.content || '';
likes = item.comment_likes || 0;
author = item.user_nickname || '匿名';
sentiment = item.sentiment || 'neutral';
tags = item.tags || [];
ip = item.ip_location || '';
} else if (type === 'drama') {
content = item.desc || '';
likes = item.statistics?.digg_count || 0;
author = item.author?.nickname || '匿名';
sentiment = 'neutral';
tags = [];
ip = '';
} else {
content = item.desc || item.content || '';
likes = item.statistics?.digg_count || 0;
author = item.author?.nickname || '匿名';
sentiment = item.sentiment || 'neutral';
tags = [];
ip = '';
}
const pColor = platformColors[type] || '#1A1A1A';
const pIcon = platformIcons[type] || '抖';
return `
${pIcon}
${esc(author)}
${ip ? `· ${esc(ip)}` : ''}
${type !== 'drama' && type !== 'douyin' && sentiment !== 'neutral' ? `● ${sentiment}` : ''}
♥${fmt(likes)}
"${esc(truncate(content, 150))}"
${tags.length ? `
${tags.slice(0,3).map(t=>`${esc(t)}`).join('')}
` : ''}
`;
}
function barChart(rows, accent) {
const max = Math.max(...rows.map(r=>r.value||0),1);
const barH=36,gap=14,padL=160,padR=100,padV=16,W=900;
const totalH = rows.length*(barH+gap)+padV*2;
return `
`;
}
function hypothesisBar(hypothesisCoverage, totalNotes) {
return Object.entries(HYPOTHESES).map(([h, info]) => {
const count = hypothesisCoverage[h] || 0;
const pct = totalNotes ? Math.round((count / totalNotes) * 100) : 0;
return `
${h} ${info.title}
${count}条 · ${pct}%
`;
}).join('');
}
// ============ 主报告生成 ============
function generateReport(data) {
const { xhsAna, dyAna, dramaAna } = data;
const { stats: xhsStats, sentiment: xhsSent, hypothesisCoverage, topTags, topNotes, brandStats, totalNotes, totalComments } = xhsAna;
const { stats: dyStats, kwStats, topVideos: dyTop, totalVideos: totalDyVideos } = dyAna;
const { stats: dramaStats, topVideos: dramaTop, totalDramas, genreStats } = dramaAna;
const totalVOC = totalNotes + totalComments + totalDyVideos + totalDramas;
const totalLikes = xhsStats.likes + dyStats.likes + dramaStats.likes;
const competitors = [
{name:'天鹅到家',revenue:'28-32亿',strength:'全国第一/平台模式/AI驱动',借鉴:'标准化体系+多渠道组合',color:'#3B82F6'},
{name:'好孕妈妈',revenue:'18-22亿',strength:'医疗背景/医院合作强/CRM',借鉴:'医院深度合作是最高效场景',color:'#00DC82'},
{name:'多喜娃',revenue:'3-4.5亿',strength:'深圳第二/自营+医院地推',借鉴:'自营模式+医院地推三步走',color:'#F97316'},
{name:'妈咪无忧',revenue:'7-9亿',strength:'上海第一/小红书KOS矩阵',借鉴:'账号矩阵打法',color:'#FF4D8D'},
];
const geoData = [
{label:'广东',value:451},{label:'江苏',value:394},{label:'山东',value:273},
{label:'浙江',value:245},{label:'湖南',value:232},{label:'广西',value:209},
];
const dyKeywordRows = Object.entries(kwStats)
.sort((a, b) => b[1].topLikes - a[1].topLikes)
.slice(0, 8)
.map(([kw, s]) => ({ label: kw, value: s.topLikes, color: '#F97316' }));
const sentimentBars = Object.entries(xhsSent).map(([s, cnt]) => {
const colors = {positive:'#00DC82',negative:'#EF4444',neutral:'#6B6B6B',conflicted:'#F97316'};
const labels = {positive:'正向',negative:'负向',neutral:'中立',conflicted:'矛盾'};
const pct = totalNotes ? Math.round((cnt / totalNotes) * 100) : 0;
return ``;
}).join('');
const tagCloud = topTags.slice(0, 12).map(([tag, cnt]) =>
`
${esc(tag)}
${cnt}
`
).join('');
const xhsBrandsSorted = Object.entries(brandStats).sort((a, b) => b[1].likes - a[1].likes);
const brandRows = xhsBrandsSorted.map(([brand, s]) =>
`| ${esc(brand)} |
${s.notes} |
${s.comments} |
♥ ${fmt(s.likes)} |
`
).join('');
const positives = topNotes.filter(n => n.sentiment === 'positive').slice(0, 4);
const negatives = topNotes.filter(n => n.sentiment === 'negative').slice(0, 4);
const neutrals = topNotes.filter(n => n.sentiment === 'neutral').slice(0, 4);
const dramaGenreRows = Object.entries(genreStats).map(([genre, cnt]) =>
`
${cnt}
${esc(genre.replace('短剧',''))}
`
).join('');
return `
洪城到家 · VOC 深度洞察报告 v4
VOC 深度洞察报告 v4
洪城到家
基于小红书 + 抖音真实数据
三大战略:抄作业 · 短剧破圈 · VOC新机会
小红书 ${totalNotes}笔记
${totalComments} 条评论
${totalDyVideos} 抖音视频
${totalDramas} 短剧
📊 ${fmt(totalVOC)} 总VOC
♥ ${fmt(totalLikes)} 总互动
📅 ${new Date().toLocaleDateString('zh-CN')}
CHAPTER 01
执行摘要
核心问题与机会分析
VOC总量
${fmt(totalVOC)}
条真实数据
三大战略方向
1️⃣
抄作业
学妈咪无忧小红书矩阵 + 学多喜娃医院地推 + 学好孕妈妈朋友圈投放 + 学天鹅到家标准化
2️⃣
短剧破圈
借南昌万亿短剧市场政策东风,低成本实现品牌曝光和流量导流
3️⃣
VOC驱动
围绕专业度、价格透明度、服务保障三大痛点构建差异化价值
CHAPTER 02
竞品获客策略
头部品牌VOC拆解与可借鉴点
${competitors.map(c=>`
`).join('')}
${dyKeywordRows.length > 0 ? `
抖音关键词热度(TOP点赞)
${barChart(dyKeywordRows, '#F97316')}
` : ''}
CHAPTER 03
用户痛点VOC
选择月嫂的三大顾虑 + H假设验证
H假设覆盖分析
${Object.entries(HYPOTHESES).slice(0,4).map(([h, info]) => {
const count = hypothesisCoverage[h] || 0;
const pct = totalNotes ? Math.round((count / totalNotes) * 100) : 0;
return `
${h} ${info.title}
${count}条
`;
}).join('')}
▲ 正面评价样本
${positives.map(v=>vocCard(v,'xhs-note')).join('')}
▼ 负面评价样本(需关注)
${negatives.length ? negatives.map(v=>vocCard(v,'xhs-note')).join('') : '
暂无负面VOC
'}
CHAPTER 04
短剧营销 × 破圈策略
借南昌万亿短剧市场政策红利
总点赞
♥${fmt(dramaStats.likes)}
互动
最高点赞
♥${fmt(dramaStats.topLikes)}
单条
⚠️
数据说明(破除幻觉)
• 短剧VOC ≠ 短剧ROI:${totalDramas}条短剧数据反映用户对内容的反应,不等于广告转化效果
• 高点赞短剧(♥${fmt(dramaStats.topLikes)})验证了"月嫂+婆媳"题材的流量基础
• 建议:以小规模测试验证ROI后再规模化投放
高赞短剧精选
${dramaTop.slice(0,4).map(v=>vocCard(v,'drama')).join('')}
题材选择
婆媳关系 + 月嫂专业 + 产后情绪,是爆款公式
CHAPTER 05
用户地理分布
小红书评论IP定位
${barChart(geoData.map(g=>({label:g.label,value:g.value,color:'#00DC82'})),'#00DC82')}
关键发现
● 广东 最大评论来源省
● 江苏 江浙沪经济发达区
● 山东 北方第一人口大省
⚠ 江西本省评论较少,需加强本地推广
CHAPTER 06
增长机会 × 新渠道
基于VOC发现的三大突破口
🏥
医院地推
直接触达即将生产的精准用户,转化率远高于泛流量
H1 医院地推
🎬
短剧营销
借南昌万亿短剧政策红利,低成本品牌曝光
H3 短剧营销
📱
多渠道组合
抖音+小红书+私域,老带新提升LTV
H7 搜索主力
CHAPTER 07
4P 配称蓝图
落地执行计划
1
阶段一(1-3月)
• 抖音/小红书账号矩阵搭建
• 与南昌3-5家核心医院产科建立合作
• 建立星级月嫂体系
2
阶段二(3-6月)
• 短视频日常运营(3-5条/周)
• 首部合作短剧(3-5万)
• 微信私域搭建+老带新
• 小红书投流测试
3
阶段三(6-12月)
• 多渠道付费投放规模化
• 短剧系列化(3-5部)
• 覆盖南昌80%+核心医院
• 品牌定制短剧(20-50万)
CHAPTER 08
VOC 高赞精选
真实用户声音
▲ 正面评价(高赞)
${positives.map(v=>vocCard(v,'xhs-note')).join('')}
● 中性评价(高赞)
${neutrals.map(v=>vocCard(v,'xhs-note')).join('')}
${dyTop && dyTop.length > 0 ? `
🎬 抖音高赞视频精选
${dyTop.slice(0,4).map(v=>vocCard(v,'douyin')).join('')}
` : ''}
`;
}
function main() {
console.log('📊 加载数据...');
const xhs = loadXHSData();
const dy = loadDouyinData();
const drama = loadDramaData();
console.log(` XHS: ${xhs.stats.notes}笔记/${xhs.stats.comments}评论`);
console.log(` 抖音: ${dy.stats.videos}视频`);
console.log(` 短剧: ${drama.stats.videos}视频(最高♥${fmt(drama.stats.topLikes)})`);
console.log('🔧 分析数据...');
const xhsAna = analyzeXHS(xhs);
const dyAna = analyzeDouyin(dy);
const dramaAna = analyzeDrama(drama);
console.log('📝 生成HTML报告...');
const html = generateReport({ xhsAna, dyAna, dramaAna });
fs.writeFileSync(OUT_FILE, html, 'utf8');
console.log(`✅ 报告已生成: ${OUT_FILE}`);
}
if (require.main === module) {
try { main(); } catch (e) { console.error('fatal:', e); process.exit(1); }
}
module.exports = { loadXHSData, loadDouyinData, loadDramaData, analyzeXHS, analyzeDouyin, analyzeDrama };