feat: 官网代码全量更新 (2026-04-18)

这个提交包含在:
wx
2026-04-18 15:13:28 +08:00
父节点 fcac8f88db
当前提交 f91ecbb093
共修改 135 个文件,包含 10307 行新增和 3349 行删除
+686
查看文件
@@ -0,0 +1,686 @@
{
"matched": [
{
"file": "0b19bf9210cd92b3ad79d1f655215e5c_compress.jpg",
"id": 16,
"author": "赵女士",
"sim": "0.930",
"lcs": 37
},
{
"file": "0c1f340ecb12fe575ecb8b60cd6064d1_compress.jpg",
"id": 17,
"author": "陈女士",
"sim": "0.948",
"lcs": 52
},
{
"file": "1791809e82abd876e5ac41c1ac910ef1_compress.jpg",
"id": 18,
"author": "朱先生",
"sim": "0.945",
"lcs": 80
},
{
"file": "334ce52997d8ef75863609ce26d4fdf5_origin.jpg",
"id": 75,
"author": "c先生",
"sim": "0.954",
"lcs": 65
},
{
"file": "3d1d2f434b72236816f9a47d47b85ad8_compress.jpg",
"id": 19,
"author": "棉花糖妈妈",
"sim": "0.915",
"lcs": 33
},
{
"file": "3fb0fb8d0425878f43af8dd652d0586b_compress.jpg",
"id": 20,
"author": "周女士一家",
"sim": "0.939",
"lcs": 44
},
{
"file": "42e05c4d66c2a19b46f60da90549ac43_origin.jpg",
"id": 76,
"author": "嘎先生",
"sim": "0.949",
"lcs": 49
},
{
"file": "45d3684da4d526647cc73c9103bd33e6_compress.jpg",
"id": 21,
"author": "雷先生",
"sim": "0.922",
"lcs": 42
},
{
"file": "58e57f980b319b93fd9ee6459328daab_origin.jpg",
"id": 77,
"author": "辛女士",
"sim": "0.870",
"lcs": 23
},
{
"file": "88b91d9e58d943c5c5a6c6227d1ca1c9_compress.jpg",
"id": 22,
"author": "关女士",
"sim": "0.928",
"lcs": 42
},
{
"file": "8c5cf2b19c6e2be899cbcd902119b95e_compress.jpg",
"id": 23,
"author": "罗女士一家",
"sim": "0.964",
"lcs": 37
},
{
"file": "94a7dd449f2399091a297bcf84741548_origin.jpg",
"id": 78,
"author": "周女士夫妇",
"sim": "0.961",
"lcs": 33
},
{
"file": "98d2a65a6975c2bbe6ed3aa1f7dc1cdf_origin.jpg",
"id": 73,
"author": "r女士",
"sim": "0.602",
"lcs": 10
},
{
"file": "a13e9aff52c7430ea1174236d22f9297_origin.jpg",
"id": 13,
"author": "没烦恼追评",
"sim": "0.972",
"lcs": 96
},
{
"file": "a17027179fbeb3981fd46971328561b5_compress.jpg",
"id": 24,
"author": "桐桐妈",
"sim": "0.856",
"lcs": 56
},
{
"file": "a437f9ac0c2b0e08c44c3d42da1ffea8_compress.jpg",
"id": 25,
"author": "钱先生一家",
"sim": "0.942",
"lcs": 29
},
{
"file": "ac05172777e8408851b13d60c79e6e80_origin.jpg",
"id": 12,
"author": "李先生",
"sim": "1.000",
"lcs": 109
},
{
"file": "b4e2bd6fd58744855111dc81d200c39d_compress.jpg",
"id": 26,
"author": "呷呷妈",
"sim": "0.892",
"lcs": 38
},
{
"file": "c39f5a9276bf3b044f13de7d5cff40cc_compress.jpg",
"id": 79,
"author": "闺蜜游客",
"sim": "0.875",
"lcs": 44
},
{
"file": "c5c7c17a8a824f08ad52a32becfccad4_compress.jpg",
"id": 27,
"author": "高先生",
"sim": "0.924",
"lcs": 35
},
{
"file": "c74e26f7638d643d29a801314f5da723_compress.jpg",
"id": 28,
"author": "蒙蒙妈妈",
"sim": "0.874",
"lcs": 41
},
{
"file": "cafbfdcfd2456a7979dc155163c51d3b_origin.jpg",
"id": 80,
"author": "毅先生",
"sim": "0.977",
"lcs": 43
},
{
"file": "e3d4809e48b01e31ad6c0673a98dbe02_origin.jpg",
"id": 14,
"author": "张先生",
"sim": "0.673",
"lcs": 47
},
{
"file": "e5e7c3eb3df3a8734ba7488f6d36d164_compress.jpg",
"id": 29,
"author": "祯荣妈妈",
"sim": "0.840",
"lcs": 26
},
{
"file": "e8d9bdc81d8d710aa6adf20f50e9a3b7_compress.jpg",
"id": 30,
"author": "Candy",
"sim": "1.000",
"lcs": 31
},
{
"file": "ef9d4a49cde5193d7299c1e914a17d66_compress.jpg",
"id": 31,
"author": "Felix",
"sim": "0.971",
"lcs": 49
},
{
"file": "fc96e0b4f96abb8c3a7cf1f32de1794e_compress.jpg",
"id": 32,
"author": "Ane一家",
"sim": "0.921",
"lcs": 28
},
{
"file": "fd873bd04436aa306ce3b070e25adb44_origin.jpg",
"id": 81,
"author": "Z女士",
"sim": "0.704",
"lcs": 11
},
{
"file": "wecom-temp-419154-4ac66331d4a43772c82e77725db680b3.jpg",
"id": 69,
"author": "大宝妈",
"sim": "1.000",
"lcs": 94
},
{
"file": "wecom-temp-435062-97aa96f921cfa00135c1728042aa31c8.jpg",
"id": 70,
"author": "小鱼儿妈妈",
"sim": "0.924",
"lcs": 34
},
{
"file": "wecom-temp-481548-1f571ea7a2ab1cf9fb43057b882e205c.jpg",
"id": 2,
"author": "Ω家庭",
"sim": "0.971",
"lcs": 66
},
{
"file": "wecom-temp-490119-b9f6b0091f6d1a196fc95f4e15d0330c.jpg",
"id": 33,
"author": "薇妈妈",
"sim": "0.881",
"lcs": 20
},
{
"file": "wecom-temp-502192-4539470cda365999985b388fd4720d65.jpg",
"id": 34,
"author": "LALA妈妈",
"sim": "1.000",
"lcs": 90
},
{
"file": "wecom-temp-503109-7659a5952dcd1655b399e3ea7f0519e4.jpg",
"id": 71,
"author": "张女士",
"sim": "0.947",
"lcs": 44
},
{
"file": "wecom-temp-504562-b5da57fcee1fca3880ed46538bce686f.jpg",
"id": 35,
"author": "Amelie",
"sim": "0.919",
"lcs": 30
},
{
"file": "wecom-temp-521988-464aadf8fbb5348973454dc5c3ce0edc.jpg",
"id": 10,
"author": "原女士",
"sim": "0.962",
"lcs": 43
},
{
"file": "wecom-temp-525346-7055c880e7855056568fbe2d9a625b53.jpg",
"id": 36,
"author": "静女士",
"sim": "0.877",
"lcs": 25
},
{
"file": "wecom-temp-525578-d98fdbb16236a439b78d8c05d4f8e076.jpg",
"id": 37,
"author": "三戒妈妈",
"sim": "0.953",
"lcs": 46
},
{
"file": "wecom-temp-544047-e05137a11e8f61dcb65927b5e6dab858.jpg",
"id": 38,
"author": "柯先生一家",
"sim": "0.928",
"lcs": 29
},
{
"file": "wecom-temp-545036-5740f3393276aaca988e0b253d0f6944.jpg",
"id": 72,
"author": "Y先生",
"sim": "0.987",
"lcs": 40
},
{
"file": "wecom-temp-550297-e3939c4fcb2b3e1360de5ec6407ef053.jpg",
"id": 39,
"author": "十二月妈妈",
"sim": "0.814",
"lcs": 24
},
{
"file": "wecom-temp-553214-4ff2fccb8929ee03d08aee30cc6cff2b.jpg",
"id": 40,
"author": "赵先生两家",
"sim": "0.972",
"lcs": 74
},
{
"file": "wecom-temp-555048-149bc9181ef19fe402982a0f7f648864.jpg",
"id": 41,
"author": "菜园子妈妈",
"sim": "0.952",
"lcs": 52
},
{
"file": "wecom-temp-561206-ae6666d289913c57dc9b36c7b49b2ed4.jpg",
"id": 42,
"author": "若澜妈妈",
"sim": "0.970",
"lcs": 33
},
{
"file": "wecom-temp-584234-1584a7c0dd02d7cae87ee8ea0894a0d8.jpg",
"id": 43,
"author": "玉兰阿姨",
"sim": "0.898",
"lcs": 31
},
{
"file": "wecom-temp-585768-2be1778ad73d286307a755e9459f7cd7.jpg",
"id": 9,
"author": "丁师傅粉丝",
"sim": "0.921",
"lcs": 53
},
{
"file": "wecom-temp-592281-c2738900291c883a6b93984423fb0af4.jpg",
"id": 44,
"author": "碧凡妈妈",
"sim": "0.928",
"lcs": 39
},
{
"file": "wecom-temp-596023-59e0c792bffb364e7bf512341642d8f2.jpg",
"id": 7,
"author": "曹女士",
"sim": "0.982",
"lcs": 108
},
{
"file": "wecom-temp-616976-3f84acc8785c4292acbe9620d2978232.jpg",
"id": 45,
"author": "一路向东团",
"sim": "0.871",
"lcs": 22
},
{
"file": "wecom-temp-617524-e739bbb5999b85dd6f21f13086e3c0a6.jpg",
"id": 46,
"author": "一寸阳光妈妈",
"sim": "0.850",
"lcs": 43
},
{
"file": "wecom-temp-620896-3d24558c7cf12e5fb5c40ae2fd5a5726.jpg",
"id": 47,
"author": "张女士一家",
"sim": "0.951",
"lcs": 53
},
{
"file": "wecom-temp-626751-692083fd684d85630b871e335c91e70e.jpg",
"id": 48,
"author": "程先生",
"sim": "0.876",
"lcs": 32
},
{
"file": "wecom-temp-627910-4e27de8f57a6b438c2240cd80cfd3302.jpg",
"id": 8,
"author": "瞳瞳妈",
"sim": "0.905",
"lcs": 51
},
{
"file": "wecom-temp-635897-20e4bc018a20199ee1ed9f2f61519b1f.jpg",
"id": 1,
"author": "圆满妈妈",
"sim": "0.929",
"lcs": 27
},
{
"file": "wecom-temp-641196-1fdadf651676e5ab293f9ceeb0335f3b.jpg",
"id": 49,
"author": "阳光妈妈",
"sim": "0.943",
"lcs": 33
},
{
"file": "wecom-temp-661813-099678966df22309d810bd3c565eb101.jpg",
"id": 5,
"author": "文小姐",
"sim": "0.957",
"lcs": 66
},
{
"file": "wecom-temp-671894-2760ea820f44bdff3e16e0b959bc550e.jpg",
"id": 50,
"author": "林先生一家",
"sim": "0.891",
"lcs": 33
},
{
"file": "wecom-temp-673334-20fec406b7b86ae33d59a439d466baf2.jpg",
"id": 6,
"author": "寒先生",
"sim": "0.835",
"lcs": 40
},
{
"file": "wecom-temp-678171-1385884e56708e21b3375bff8da5b934.jpg",
"id": 51,
"author": "选香妈妈",
"sim": "0.863",
"lcs": 27
},
{
"file": "wecom-temp-699431-aae9dd81c4714ed556f77d3779b39522.jpg",
"id": 52,
"author": "张先生一家七口",
"sim": "0.888",
"lcs": 30
},
{
"file": "wecom-temp-700245-24de132a6ce843069453f960c6092fb5.jpg",
"id": 53,
"author": "朱先生一家",
"sim": "0.929",
"lcs": 71
},
{
"file": "wecom-temp-701910-25f879cfe5b15e2c8b453994bcfb814c.jpg",
"id": 54,
"author": "William一家",
"sim": "0.919",
"lcs": 37
},
{
"file": "wecom-temp-703242-a249c3dbe803d922595ce05f44f18345.jpg",
"id": 55,
"author": "黄女士母女",
"sim": "0.870",
"lcs": 24
},
{
"file": "wecom-temp-704580-78bca81e19de1be5830f18ca5815bad4.jpg",
"id": 56,
"author": "22人大团",
"sim": "0.862",
"lcs": 55
},
{
"file": "wecom-temp-706277-519f7d4e50261174fed885d7969e00b9.jpg",
"id": 74,
"author": "函女士",
"sim": "0.921",
"lcs": 30
},
{
"file": "wecom-temp-709594-1148d89600000f4c21ea7e4e88ff6ee0.jpg",
"id": 57,
"author": "吴女士一家",
"sim": "0.897",
"lcs": 31
},
{
"file": "wecom-temp-714060-652ccbfafcb1e0a9baefa7e9801269f0.jpg",
"id": 58,
"author": "琳妈妈",
"sim": "0.858",
"lcs": 19
},
{
"file": "wecom-temp-718278-8c9138292153639aaa0e952ace6dac72.jpg",
"id": 59,
"author": "王先生一家",
"sim": "0.827",
"lcs": 30
},
{
"file": "wecom-temp-723725-35dcff939aa86255168a297fc2b6ce94.jpg",
"id": 60,
"author": "三叉妈妈",
"sim": "0.849",
"lcs": 26
},
{
"file": "wecom-temp-727194-5bca34a2f2e0e55264fbd5f52f8343d2.jpg",
"id": 61,
"author": "暖阳先生",
"sim": "0.917",
"lcs": 29
},
{
"file": "wecom-temp-737319-f3701932a5141ee881ade5181fe96a18.jpg",
"id": 62,
"author": "Sissi妈妈",
"sim": "0.841",
"lcs": 21
},
{
"file": "wecom-temp-743626-7451ea4e9b865f4fe5f1c2c2f4e15c7c.jpg",
"id": 63,
"author": "娃妈妈",
"sim": "0.897",
"lcs": 25
},
{
"file": "wecom-temp-757088-579a7c14f92a5f7620f5fb58c62ef373.jpg",
"id": 64,
"author": "丹子妈妈",
"sim": "0.594",
"lcs": 28
},
{
"file": "wecom-temp-769494-19e375ad2558104eb6a0330b89878b9d.jpg",
"id": 65,
"author": "一对小情侣",
"sim": "0.719",
"lcs": 26
},
{
"file": "wecom-temp-771995-5193841c2a1f38c6e84491ae8a73a294.jpg",
"id": 66,
"author": "旅拍妈妈",
"sim": "0.703",
"lcs": 16
},
{
"file": "wecom-temp-810529-029024f7cd650dd6f7a863644afce957.jpg",
"id": 67,
"author": "张妈妈一家",
"sim": "0.878",
"lcs": 28
},
{
"file": "wecom-temp-833880-be4ca8260e3a3b390d9db2ffa58286eb.jpg",
"id": 3,
"author": "胡女士",
"sim": "0.954",
"lcs": 73
},
{
"file": "wecom-temp-834555-028e05ad9e8c13613f6b9d2b7886cb89.jpg",
"id": 4,
"author": "G先生",
"sim": "0.573",
"lcs": 40
},
{
"file": "wecom-temp-840703-36897967667180d51c7c945165f991b6.jpg",
"id": 82,
"author": "学妈妈",
"sim": "0.812",
"lcs": 18
},
{
"file": "wecom-temp-956662-3db5e12c2fcd2be8f5b1a96ddbd4eeaa.jpg",
"id": 68,
"author": "思敏一家",
"sim": "0.836",
"lcs": 16
}
],
"filteredPhone": [
{
"file": "0488a1ca3930767030b85edcdd66e2de_compress.jpg",
"snippet": "13304704622"
},
{
"file": "1cce83f62f62676487be19cd16142f5d_compress.jpg",
"snippet": "15048078833"
},
{
"file": "5fb2b38d4dd316b3241630ee668af91b_compress.jpg",
"snippet": "13474949217"
},
{
"file": "wecom-temp-533490-804186f885b0dc8bd5d2fd23577c430e.jpg",
"snippet": "13351887886"
},
{
"file": "wecom-temp-578014-117a6a370000db1e22c956f489abba5e.jpg",
"snippet": "13848806788"
},
{
"file": "wecom-temp-644701-3e84a46d8bbdbaef318a6caa7fa1aea2.jpg",
"snippet": "13314816186"
},
{
"file": "wecom-temp-887239-5cabf29201fb607ac57a98355c4addc4.jpg",
"snippet": "19845234524"
}
],
"filteredNeg": [
{
"file": "wecom-temp-636568-5493492a0c36ca7392827b776077dcc8.jpg",
"keyword": "骗人"
}
],
"unmatched": [
{
"file": "a3f1ab93efcc7e7652f69fe1b4c8b392_origin.jpg",
"bestId": 13,
"sim": "0.983",
"reason": "dup"
},
{
"file": "wecom-temp-569374-ce80bc9a9a97b950cf5773076f91aa60.jpg",
"bestId": 33,
"sim": "0.073"
},
{
"file": "wecom-temp-571943-8a2b1eb68775a3c0f4b7de703651a621.jpg",
"bestId": 55,
"sim": "0.167"
},
{
"file": "wecom-temp-588120-9c19ae1e7e15a3a85f321d19f9904966.jpg",
"bestId": 72,
"sim": "0.120"
},
{
"file": "wecom-temp-590828-462615cdeae72e1492499bdb09f1f22f.jpg",
"bestId": 71,
"sim": "0.253"
},
{
"file": "wecom-temp-600896-24ba3e956b95fb17e46b3993f4d31690.jpg",
"bestId": 50,
"sim": "0.125"
},
{
"file": "wecom-temp-627802-41c1ee6392b619b5e05cf5142d98cfd2.jpg",
"bestId": 73,
"sim": "0.953",
"reason": "dup"
},
{
"file": "wecom-temp-640695-fb25871a63c7c394272cfb52cc3c77ce.jpg",
"bestId": 22,
"sim": "0.174"
},
{
"file": "wecom-temp-659848-6d4ed48397eef7a701ca334a6efc66cf.jpg",
"bestId": 10,
"sim": "0.057"
},
{
"file": "wecom-temp-781799-815e4e89ac263183d05371614183cb99.jpg",
"bestId": 23,
"sim": "0.959",
"reason": "dup"
},
{
"file": "wecom-temp-787055-3378be413159c8cce2f0b1ef546ca332.jpg",
"bestId": 64,
"sim": "0.819",
"reason": "dup"
},
{
"file": "wecom-temp-787279-865a62d50ebac49dbc5aec976bb18ed2.jpg",
"bestId": 65,
"sim": "0.437",
"reason": "dup"
},
{
"file": "wecom-temp-854504-6524884902833847963a4c0c92669e0e.jpg",
"bestId": 2,
"sim": "0.971",
"reason": "dup"
},
{
"file": "wecom-temp-855930-31bd36358a50269626d9b1db9d4db482.jpg",
"bestId": 2,
"sim": "0.971",
"reason": "dup"
},
{
"file": "wecom-temp-896552-41c8f285adda9e422051cc88d117bf32.jpg",
"bestId": 1,
"sim": "0.929",
"reason": "dup"
}
],
"ocrFailed": []
}
+67
查看文件
@@ -0,0 +1,67 @@
#!/usr/bin/env node
/**
* 基于评价正文关键词,自动回填 reviews.scenes 和 reviews.concerns。
* 仅在字段为空时写入,已人工打过的保留。
*/
import Database from 'better-sqlite3'
import { dirname, join } from 'node:path'
import { fileURLToPath } from 'node:url'
const DB = join(dirname(fileURLToPath(import.meta.url)), '../server/database/hulai.db')
// 场景词表:关键词命中 → 标签
const SCENE_RULES = [
{ tag: '亲子', kws: ['孩子', '小朋友', '娃', '宝宝', '女儿', '儿子', '孩儿', '带娃'] },
{ tag: '家庭出游', kws: ['全家', '一家', '家人', '爸妈', '父母', '夫妻'] },
{ tag: '带老人', kws: ['老人', '父母', '爸妈', '长辈', '外婆', '奶奶', '爷爷', '姥姥'] },
{ tag: '蜜月', kws: ['蜜月', '度蜜月', '情侣'] },
{ tag: '摄影', kws: ['旅拍', '拍照', '摄影', '出片', '壁纸'] },
{ tag: '毕业游', kws: ['毕业', '升学', '小升初', '幼升小'] },
{ tag: '多家庭', kws: ['两个家庭', '三家', '朋友一起', '两家', '同行', '大团', '几家', '好友', '闺蜜', '同伴', '一起出行', '结伴'] },
]
// 关心维度词表
const CONCERN_RULES = [
{ tag: '领队服务', kws: ['领队', '师傅', '司机', '导游', '贴心', '耐心', '细心'] },
{ tag: '行程安排', kws: ['行程', '安排', '线路', '路线', '节奏'] },
{ tag: '住宿体验', kws: ['住宿', '酒店', '民宿', '房间', '房'] },
{ tag: '摄影旅拍', kws: ['旅拍', '拍照', '摄影', '出片', '服饰'] },
{ tag: '错峰不排队', kws: ['错峰', '不排队', '避开', '人少', '小众', '清净', '避人', '安静', '没有人', '没什么人'] },
{ tag: '性价比', kws: ['性价比', '值得', '划算', '物超所值', '实惠', '不贵', '合适', '值', '不亏'] },
{ tag: '安全放心', kws: ['安全', '放心', '安心', '靠谱', '信任'] },
{ tag: '特色体验', kws: ['骑马', '越野车', '营地', '烧烤', '滑草', '漂流', '卡丁车'] },
{ tag: '饮食', kws: ['吃', '餐', '美食', '饭菜', '零食', '水果'] },
]
function tagsFor(content, rules) {
const out = []
for (const { tag, kws } of rules) {
if (kws.some(k => content.includes(k))) out.push(tag)
}
return out
}
const db = new Database(DB)
const rows = db.prepare('SELECT id, content, scenes, concerns FROM reviews').all()
const upd = db.prepare('UPDATE reviews SET scenes = ?, concerns = ? WHERE id = ?')
let touched = 0
const tx = db.transaction(() => {
for (const r of rows) {
const c = r.content || ''
upd.run(JSON.stringify(tagsFor(c, SCENE_RULES)), JSON.stringify(tagsFor(c, CONCERN_RULES)), r.id)
touched++
}
})
tx()
console.log(`更新了 ${touched} / ${rows.length} 条评价的场景/关心点标签`)
// 统计
const allScenes = new Set()
const allConcerns = new Set()
for (const r of db.prepare('SELECT scenes, concerns FROM reviews').all()) {
try { JSON.parse(r.scenes || '[]').forEach(x => allScenes.add(x)) } catch {}
try { JSON.parse(r.concerns || '[]').forEach(x => allConcerns.add(x)) } catch {}
}
console.log('场景标签:', [...allScenes].join(' · '))
console.log('关心维度:', [...allConcerns].join(' · '))
+174
查看文件
@@ -0,0 +1,174 @@
#!/usr/bin/env node
/**
* 批量 OCR 评价截图 → 匹配 DB 评价 → 写入 screenshot 字段
*
* 流程:
* 1. 扫描 public/uploads/reviews/ 的 jpg
* 2. tesseract chi_sim OCR
* 3. 过滤:包含手机号(1[3-9]\d{9})或负面关键词的截图丢弃
* 4. 规整文本,按 n-gram 交集相似度匹配 82 条 DB 评价
* 5. 命中 → UPDATE reviews SET screenshot=/uploads/reviews/xxx.jpg
* 6. 未命中 → 报告供人工处理
*
* 不自动创建新评价:避免 OCR 噪声/差评混入库。
*/
import Database from 'better-sqlite3'
import { execFileSync } from 'node:child_process'
import { readdirSync, existsSync, writeFileSync } from 'node:fs'
import { join, dirname } from 'node:path'
import { fileURLToPath } from 'node:url'
const __dirname = dirname(fileURLToPath(import.meta.url))
const ROOT = join(__dirname, '..')
const DB_PATH = join(ROOT, 'server/database/hulai.db')
const SHOTS_DIR = join(ROOT, 'public/uploads/reviews')
const REL_PREFIX = '/uploads/reviews/'
const PHONE_RE = /\b1[3-9]\d{9}\b/
const NEG_KEYWORDS = ['差评', '投诉', '退款', '退一赔', '不满意', '很失望', '骗人', '被骗', '黑心', '垃圾', '坑人', '太烂', '烂透', '糟糕', '欺诈', '维权', '曝光']
function ocr(imagePath) {
try {
const out = execFileSync('tesseract', [imagePath, 'stdout', '-l', 'chi_sim'], {
encoding: 'utf8',
stdio: ['ignore', 'pipe', 'ignore'],
maxBuffer: 10 * 1024 * 1024,
})
return out
} catch {
return ''
}
}
// 清理 OCR 文本:去空白/标点/数字/半角字母,只留中文
function canonical(s) {
return (s || '').replace(/[^\u4e00-\u9fa5]/g, '')
}
// 生成 bigram 集合
function bigrams(s) {
const set = new Set()
for (let i = 0; i < s.length - 1; i++) set.add(s.slice(i, i + 2))
return set
}
// bigram Jaccard 相似度(0~1)
function similarity(a, b) {
if (!a || !b) return 0
const A = bigrams(a), B = bigrams(b)
if (A.size === 0 || B.size === 0) return 0
let inter = 0
for (const x of A) if (B.has(x)) inter++
return inter / Math.min(A.size, B.size) // use min instead of union: 截图文本往往比润色后的 DB 内容多噪声
}
// 最长公共子串长度(粗粒度,用于 tie-break)
function lcsLen(a, b) {
// 限制长度避免 O(n²) 爆炸
if (a.length > 500) a = a.slice(0, 500)
if (b.length > 500) b = b.slice(0, 500)
const n = a.length, m = b.length
if (!n || !m) return 0
let prev = new Array(m + 1).fill(0)
let curr = new Array(m + 1).fill(0)
let best = 0
for (let i = 1; i <= n; i++) {
for (let j = 1; j <= m; j++) {
curr[j] = a[i - 1] === b[j - 1] ? prev[j - 1] + 1 : 0
if (curr[j] > best) best = curr[j]
}
;[prev, curr] = [curr, prev]
}
return best
}
const db = new Database(DB_PATH)
const reviews = db.prepare('SELECT id, author, content, screenshot FROM reviews').all()
console.log(`DB reviews: ${reviews.length}`)
const reviewsIdx = reviews.map(r => ({ ...r, canon: canonical(r.content) }))
const files = readdirSync(SHOTS_DIR).filter(f => /\.(jpe?g|png)$/i.test(f))
console.log(`screenshots: ${files.length}\n`)
const SIM_THRESHOLD = 0.35
const LCS_MIN = 10
const results = { matched: [], filteredPhone: [], filteredNeg: [], unmatched: [], ocrFailed: [] }
const assignedReviewIds = new Set()
let i = 0
for (const file of files) {
i++
const abs = join(SHOTS_DIR, file)
process.stdout.write(`[${i}/${files.length}] ${file} ... `)
const text = ocr(abs)
if (!text || text.length < 20) {
console.log('OCR 失败/太短')
results.ocrFailed.push(file)
continue
}
if (PHONE_RE.test(text)) {
console.log('含手机号 → 跳过')
results.filteredPhone.push({ file, snippet: text.match(PHONE_RE)[0] })
continue
}
const hitNeg = NEG_KEYWORDS.find(k => text.includes(k))
if (hitNeg) {
console.log(`负面关键词「${hitNeg}」→ 跳过`)
results.filteredNeg.push({ file, keyword: hitNeg })
continue
}
const canon = canonical(text)
if (canon.length < 30) {
console.log('有效中文太少')
results.ocrFailed.push(file)
continue
}
// 评分
let best = null
for (const r of reviewsIdx) {
const sim = similarity(canon, r.canon)
if (!best || sim > best.sim) best = { ...r, sim }
}
if (best && best.sim >= SIM_THRESHOLD) {
const lcs = lcsLen(canon, best.canon)
if (lcs >= LCS_MIN) {
if (assignedReviewIds.has(best.id)) {
// 已分配给更高分的图
console.log(`评价#${best.id} 已占,放入未匹配`)
results.unmatched.push({ file, bestId: best.id, sim: best.sim.toFixed(3), reason: 'dup' })
continue
}
assignedReviewIds.add(best.id)
results.matched.push({ file, id: best.id, author: best.author, sim: best.sim.toFixed(3), lcs })
console.log(`→ #${best.id} ${best.author} sim=${best.sim.toFixed(3)} lcs=${lcs}`)
continue
}
}
console.log(`未匹配 (best sim=${best?.sim.toFixed(3)})`)
results.unmatched.push({ file, bestId: best?.id, sim: best?.sim.toFixed(3) })
}
// 写入 DB
const update = db.prepare('UPDATE reviews SET screenshot = ? WHERE id = ?')
const tx = db.transaction(rows => rows.forEach(r => update.run(REL_PREFIX + r.file, r.id)))
tx(results.matched)
// 过滤掉手机/负面的截图文件直接删除
import('node:fs').then(({ unlinkSync }) => {
for (const { file } of [...results.filteredPhone, ...results.filteredNeg]) {
try { unlinkSync(join(SHOTS_DIR, file)) } catch {}
}
})
writeFileSync(join(ROOT, 'scripts/.screenshot-import-report.json'), JSON.stringify(results, null, 2))
console.log('\n===== 汇总 =====')
console.log(`命中 DB 并写入: ${results.matched.length}`)
console.log(`含手机号已跳过删除: ${results.filteredPhone.length}`)
console.log(`负面关键词已跳过删除: ${results.filteredNeg.length}`)
console.log(`未匹配: ${results.unmatched.length}`)
console.log(`OCR 失败: ${results.ocrFailed.length}`)
console.log(`\n详细报告: scripts/.screenshot-import-report.json`)
+89
查看文件
@@ -0,0 +1,89 @@
#!/usr/bin/env node
/**
* 旅行攻略 12 篇文章 SEO 元信息批量补齐
* 标准:
* - seo_title: 带「呼伦贝尔」+ 主关键词 + 「呼籁旅行」,长度 30-50 字
* - seo_description: 120-155 字,含核心痛点 + 价值主张 + 行动暗示
* - seo_keywords: 5-8 个,按搜索热度排布,逗号分隔
*/
import Database from 'better-sqlite3'
const db = new Database('./server/database/hulai.db')
const seo = {
6: {
title: '呼伦贝尔亲子游品质出行指南 - 11年本地从业者实战经验 | 呼籁旅行',
desc: '选时间、买机票、看配置、比价格、选住宿、判断行程……深耕呼伦贝尔11年、累计服务10000+家庭的本地从业者,把亲子游常见问题和品质判断标准一次性讲清楚。适合第一次带孩子去呼伦贝尔的家庭参考。',
kw: '呼伦贝尔亲子游,品质出行,呼伦贝尔攻略,本地从业者,家庭游指南,呼籁旅行',
},
7: {
title: '呼伦贝尔亲子游年龄分层指南:2-4岁/5-7岁/8-12岁怎么玩 | 呼籁旅行',
desc: '不同年龄段的孩子,对应完全不同的行程设计。本文按 2-4 岁感官体验、5-7 岁互动参与、8-12 岁深度探索三档,拆解适合的活动、行程天数、住宿选择和注意事项,让每一个年龄段都玩得尽兴又不累。',
kw: '呼伦贝尔亲子游年龄,2-4岁草原,5-7岁亲子,8-12岁研学,亲子行程天数,呼籁亲子游',
},
8: {
title: '呼伦贝尔亲子骑马攻略:安全吗?几岁能骑?景区vs草原穿越差在哪 | 呼籁旅行',
desc: '呼伦贝尔骑马安全吗?几岁能骑?景区马场和草原穿越有什么区别?本文详解五项安全核心指标、不同年龄段的适配项目、预订前必问的 10 个问题,以及如何识别正规马场。带娃家庭必看。',
kw: '呼伦贝尔骑马,亲子骑马安全,草原穿越,景区马场,骑马年龄,呼伦贝尔马场推荐,呼籁骑马',
},
9: {
title: '呼伦贝尔旅游的两条路:在地深耕 vs 全国跑场 | 呼籁旅行行业洞察',
desc: '呼伦贝尔旅游市场正在分化成两种模式:本地团队深耕单一目的地,全国连锁品牌旺季跑场。两种模式的服务质量、价格水位、应急能力差异巨大。本文用行业内部视角帮消费者判断该选哪一种。',
kw: '呼伦贝尔旅游,在地旅行社,全国旅行社,旅游行业分析,呼伦贝尔本地团队,呼籁旅行',
},
10: {
title: '2026年呼伦贝尔旅游攻略:首次出行必看完整指南 | 呼籁旅行',
desc: '第一次去呼伦贝尔?本文涵盖推荐季节、核心景点、行程天数、交通方式、预算参考五大维度,基于 2025 年实际数据更新。帮你避开新手常犯的 6 个错误,把每一天都过得值回票价。',
kw: '呼伦贝尔旅游攻略,2026呼伦贝尔,首次出行指南,呼伦贝尔景点推荐,呼伦贝尔预算,呼籁攻略',
},
11: {
title: '呼伦贝尔亲子游综合指南:带娃怎么玩才不踩坑 | 呼籁旅行',
desc: '带孩子去呼伦贝尔草原,几岁合适?哪些活动安全?怎么选行程?呼籁旅行 11 年亲子游经验,从孩子年龄、体能、兴趣点三个维度,帮你把每一个细节都想清楚,一次不踩雷的亲子游规划指南。',
kw: '呼伦贝尔亲子游,带娃攻略,草原亲子游,呼伦贝尔亲子路线,亲子游避坑,呼籁亲子',
},
12: {
title: '呼伦贝尔几月份去合适?四季月份对比全解析 | 呼籁旅行',
desc: '6 月绿草、7 月油菜花、8 月蓝天、9-10 月金秋、12-2 月雪原……呼伦贝尔不同月份体验差异远超预期。本文用气温、景色、人流、价格四维数据对比 12 个月份,帮你选出适合自己的出行时间。',
kw: '呼伦贝尔几月去,呼伦贝尔最佳旅游季节,呼伦贝尔旺季,呼伦贝尔月份对比,呼伦贝尔错峰,呼籁旅行',
},
13: {
title: '呼伦贝尔自驾 vs 包车:哪种方式更适合你 | 呼籁旅行',
desc: '呼伦贝尔自驾还是包车?这个问题困扰着不少初次出行的旅行者。本文从路况复杂度、费用对比、自由度、安全性、带娃友好度五个维度全面分析,附上真实案例,帮你选出适合自己的出行方式。',
kw: '呼伦贝尔自驾,呼伦贝尔包车,自驾游攻略,草原包车,呼伦贝尔租车,呼籁包车',
},
14: {
title: '呼伦贝尔旅行花费明细:玩一趟到底要多少钱 | 呼籁旅行真实预算',
desc: '呼伦贝尔一趟到底花多少钱?本文拆解机票、住宿、交通、餐饮、活动、购物各项费用,按人均 3000-8000+ 分高中低三档给出真实预算参考,基于 2025 年实际订单数据统计,不夸大也不压价。',
kw: '呼伦贝尔旅游费用,呼伦贝尔人均预算,呼伦贝尔花费,呼伦贝尔包车价格,呼伦贝尔住宿,呼籁报价',
},
15: {
title: '带 4-6 岁孩子去草原的注意事项 | 呼籁旅行亲子出行指南',
desc: '4-6 岁是孩子初次体验草原的黄金年龄:好奇心旺盛但自我保护能力弱。这篇文章总结了呼籁 11 年来服务这个年龄段的全部经验:行李清单、安全项目、饮食过敏应对、行程强度控制,一次说透。',
kw: '4-6岁亲子游,草原亲子,幼儿出行,呼伦贝尔带娃,幼童旅行,草原安全,呼籁亲子',
},
16: {
title: '呼伦贝尔避坑指南:这些坑千万别踩 | 呼籁旅行真实避雷',
desc: '呼伦贝尔旅游的坑不少:强制购物、人头拼团、低价陷阱、景区宰客、假包车……本文列出 8 个最常见的坑+真实案例+识别方法。把雷都踩在出发之前,不要让好的行程毁在细节上。',
kw: '呼伦贝尔避坑,呼伦贝尔旅游陷阱,强制购物,低价团陷阱,呼伦贝尔真相,呼籁避坑',
},
17: {
title: '草原旅拍穿搭指南:怎么拍出大片感 | 呼籁旅行',
desc: '去草原不知道穿什么?本文从色系搭配(蓝天金草配什么颜色)、服装款式(连衣裙/蒙古袍/家庭一体感)、配件选择、拍摄时机(光线黄金时段)、构图技巧五个方面,手把手教你在呼伦贝尔拍出朋友圈封面级大片。',
kw: '草原旅拍穿搭,呼伦贝尔拍照,旅拍服饰,草原拍照指南,蒙古袍旅拍,呼籁旅拍',
},
}
const upd = db.prepare('UPDATE articles SET seo_title=?, seo_description=?, seo_keywords=? WHERE id=?')
const tx = db.transaction(() => {
for (const [id, s] of Object.entries(seo)) {
upd.run(s.title, s.desc, s.kw, Number(id))
}
})
tx()
console.log(`✓ 已补齐 ${Object.keys(seo).length} 篇文章的 SEO 元信息`)
// 验证
const verify = db.prepare("SELECT id,title,length(seo_title) lt, length(seo_description) ld, seo_keywords FROM articles WHERE type='blog' ORDER BY id").all()
verify.forEach(r => {
const kwCount = (r.seo_keywords || '').split(',').filter(Boolean).length
console.log(`#${r.id}: title ${r.lt}字 | desc ${r.ld}字 | ${kwCount} 个关键词`)
})
+171
查看文件
@@ -0,0 +1,171 @@
import Database from 'better-sqlite3'
const db = new Database('./server/database/hulai.db')
const stories = [
// ========== 故事 1:上海四口之家·两娃首游 ==========
{
id: 1,
slug: 'shanghai-family-four-summer-2025',
title: '上海四口之家:两个孩子、一辆车,那趟让全家都放松的旅行',
subtitle: '大女儿 8 岁,二宝 5 岁,从上海出发。去之前最担心的是孩子太小、行程太满;回来才明白原来可以让全家都放松',
summary: '两个孩子、一辆车,从上海出发的一次家庭旅行。去之前在三个目的地里犹豫了三个月,回来之后在朋友圈被问爆了推荐方案。',
author: '张女士',
sourceUrl: '',
tags: ['亲子', '四口之家', '上海出发', '夏季', '首次呼伦贝尔', '6天5晚'],
familyType: '四口之家',
childrenAge: '8岁 + 5岁',
fromCity: '上海',
tripProduct: '额吉的故乡V9 · 6天5晚亲子版',
travelDate: '2025年7月',
coverImage: '/images/gallery/gallery-001.jpg',
published: true,
sortOrder: 1,
sections: [
{
heading: '出发前的三个月',
content: '真到要订的时候,反而不敢下手了。呼伦贝尔、新疆、青海三个目的地,我在小红书和各种群里来回比较了三个月。最担心的就两件事:带两个孩子会不会很累,还有就是——定制游贵那么多,到底值不值。\n\n最后选呼籁,其实是因为定制师没急着让我下单。我把孩子的作息、我老公的腰不好、我妈问能不能带个老人一起,这些零零碎碎都告诉她,她一条一条回复,不催我。她说"您先看看方案,有什么顾虑都提出来"。这种不急着成交的感觉,反而让我觉得靠谱。'
},
{
heading: '一辆车开出去就松了',
content: '从海拉尔机场接到我们那一刻,行李还没放进车,领队就把两瓶常温矿泉水递给孩子,又给我老公递了一个颈枕。他说"路上车程有长有短,您随时说停就停"。这句话后来应验了好多次——二宝看到羊群喊停,我们停了;大女儿要上厕所,我们找了最近的牧户家;我老公腰不舒服想趴会儿,领队直接把后排放平。\n\n独立成团的意思,是真的只有你们一家在这辆车上。不用等别人、不用迁就别人、不用不好意思。我第一次真切体会到"一团一车"不是营销词。'
},
{
heading: '领队记住了我们家每个人',
content: '第三天晚上在营地吃烧烤,领队拿出来一盒冰淇淋,问二宝"还要不要吃草莓味的?"我愣了一下——这话孩子是第二天随口跟他说的,过了两天他居然还记得。\n\n还有大女儿,她不爱吃羊肉,但看到别人都在吃又不好意思说。领队看了一眼,转身让厨房单独给她做了一份小菜。他没跟我们说,是我后来看到菜单才发现。\n\n这些事一件一件不大,但叠加起来就是:我们家每个人都被看到了。这对带两个孩子的妈妈来说,真的是一种非常珍贵的"被照顾感"。'
},
{
heading: '朋友圈发出去之后',
content: '回来那晚发了一条朋友圈,配了几张孩子在草原上疯跑的照片,没配太多文字。结果那一晚私信被问爆了。\n\n我跟朋友说,这次旅行最大的收获不是看了多美的景——景当然美,但我们家两个孩子跑到哪都能跑疯,其实景色对他们来说没那么重要。真正的收获是:"这一周我没吼过孩子,我老公的腰没犯,我妈没抱怨走不动。"\n\n对带娃家庭来说,这就是最高评价。下一次,我想带着我爸妈一起再来一次,让他们也看看这片草原。'
},
],
keyMoments: [
'第一天到酒店,房间里已经准备好了儿童牙刷、儿童拖鞋和矮凳,领队说这是"默认配置"',
'二宝第一次骑马,全程有马师牵着缰绳,孩子抖着腿坚持骑完一小时,下马后的第一句话是"妈妈我还要骑"',
'大女儿在白桦林捡了一块卷曲的树皮回来,说这是她最喜欢的"草原礼物",现在还压在她的书桌玻璃下',
'最后一天离开前,领队发给我一份住宿地点清单——包括退房时间、如何联系前台退押金,"万一您回家路上有疑问可以直接联系"'
],
seoTitle: '上海一家四口呼伦贝尔6天5晚亲子游真实故事 - 呼籁旅行',
seoDesc: '上海张女士一家四口(两个孩子8岁+5岁)2025年7月呼伦贝尔6天5晚定制亲子游的真实记录,从出发前犹豫到回家推荐朋友的全过程。'
},
// ========== 故事 2:3 岁幼童真能上草原 ==========
{
id: 2,
slug: 'toddler-first-grassland-2025',
title: '带 3 岁宝宝上草原:很多人说太小了不适合,但我们去了',
subtitle: '别人都说"等孩子大一点再去",我们没听。回来之后宝宝爱上了羊,也爱上了那个陪他一整天的叔叔',
summary: '3 岁的孩子能上草原吗?出发前所有人都劝我们再等几年。我们最终还是去了,回来才知道:3 岁的孩子什么都记得。',
author: '琪琪妈',
sourceUrl: '',
tags: ['亲子', '幼童', '3岁宝宝', '首次出远门', '定制幼童友好方案'],
familyType: '三口之家',
childrenAge: '3岁',
fromCity: '杭州',
tripProduct: '额吉的故乡V9 · 5天4晚版(定制幼童友好方案)',
travelDate: '2025年6月',
coverImage: '/images/stories/placeholder-toddler.jpg',
published: true,
sortOrder: 2,
sections: [
{
heading: '所有人都说"再等等"',
content: '我婆婆不同意,我妈也不同意,连我发小都说"3 岁太小了,他什么都不记得,浪费钱"。我自己也确实犹豫——3 岁这个年纪,机场闹不闹、车程撑不撑得住、水土服不服,全都是未知数。\n\n最终还是决定去,因为我老公说了一句:"孩子小是真的小,但是他记不记得不是最重要的,我们记得就行。"\n\n我把这些顾虑一条一条发给呼籁的定制师。她没说"没问题都能搞定"这种套话,而是非常具体地回复我:每天车程控制在 2 小时以内、酒店选有婴儿床的、中午必须安排午睡、带孩子的所有高强度项目都预留"可以随时退出"的机制。她说:"3 岁的孩子我们接待过很多,但每个孩子状态都不一样,行程可以随时调。"'
},
{
heading: '领队是一个奶爸',
content: '接机的领队自己有一个 4 岁的女儿。他第一次见到宝宝就蹲下来和他平视说话,没有那种"逗小孩"的夸张劲儿,就是很平常地问:"你带了你的小汽车吗?"\n\n整个行程里,他不是把宝宝当"客户的孩子"小心翼翼地伺候着,而是真的像一个熟悉的叔叔。宝宝走累了他就蹲下来让宝宝爬上背;宝宝要撒尿他比我还快找到最近的草丛;宝宝在车上睡着,他就把车开得特别平稳,空调温度也调到我要的 24 度。\n\n这些都不是"服务",是只有带过孩子的人才会下意识做的动作。'
},
{
heading: '宝宝爱上了羊',
content: '第三天去牧户家,宝宝第一次近距离看到羊。他没有哭也没有害怕,就是蹲在羊旁边看,看了有二十多分钟。领队悄悄跟我说:"我们不催他,他看够了自己会起来。"\n\n当天下午他一路上都在说"羊""羊""羊"。晚上睡觉前他抱着我问:"妈妈,那只白色的羊明天还在吗?"\n\n回来之后他画画全都是羊。幼儿园老师问他"你最喜欢什么?"他说"草原的羊"。到现在过了半年,他还记得那只羊——所有人说"3 岁记不得"的事情,他全都记得。'
},
{
heading: '给还在犹豫的妈妈',
content: '如果你也在纠结孩子是否太小,我想分享两点:\n\n第一,3 岁的孩子不需要"看懂"什么,他需要的是"身体的记忆"——风的感觉、动物的气味、和妈妈手牵手走在一片无边无际的绿色里。这些他不会说出来,但他会用一辈子保存。\n\n第二,重点不是孩子有没有准备好,而是你找到的团队有没有准备好。呼籁为我们做的最重要的一件事,就是把行程强度降到 3 岁孩子能承受的程度——每天不赶、随时可停、所有项目都有"不玩也没关系"的出口。\n\n这不是我一个人写的评价,这是我想对所有像我一样犹豫的妈妈说的话。'
},
],
keyMoments: [
'第一天入住酒店,前台已经准备好婴儿床、婴儿浴盆和儿童洗漱用品,全程我都没开口问过',
'中午吃饭的餐厅,领队提前打电话让厨房准备了清淡的蔬菜粥和蒸蛋,不加盐、不加酱油',
'宝宝在牧户家看羊看了二十多分钟,领队和整个团队没有一个人催,一直笑眯眯地等着',
'离开那天宝宝哭了,不是因为累,是因为舍不得那个奶爸领队,一路上抱着他的脖子不松手'
],
seoTitle: '3 岁幼童呼伦贝尔草原之旅真实记录 - 呼籁旅行',
seoDesc: '3 岁宝宝能不能上草原?杭州琪琪妈一家的真实记录。从所有人反对到最终决定出发,从担心水土不服到孩子爱上羊,分享定制幼童友好方案的全过程。'
},
// ========== 故事 3:三代同堂 10 人大团 ==========
{
id: 3,
slug: 'three-gen-ten-people-2025',
title: '十人大团三代同堂:外婆第一次坐飞机,孙女第一次骑马',
subtitle: '一家十口,老老少少,从北京和深圳两地出发汇合海拉尔。老人最担心走不动,孩子最担心没得玩;走完一圈,没一个人掉队',
summary: '一家十口:爷爷奶奶、外公外婆、爸爸妈妈加三个孩子,第一次三代同行。老人能走吗?孩子能玩吗?十个人的节奏怎么统一?这是我们的答案。',
author: '罗女士',
sourceUrl: '',
tags: ['三代同游', '十人大团', '老人', '多家庭', '6天5晚', '慢节奏'],
familyType: '三代同堂 10 人',
childrenAge: '9岁+7岁+4岁',
fromCity: '北京 / 深圳',
tripProduct: '额吉的故乡V9 · 6天5晚(针对老人定制慢节奏方案)',
travelDate: '2025年8月',
coverImage: '/images/summer-camp/grassland-group.jpg',
published: true,
sortOrder: 3,
sections: [
{
heading: '十个人的出行,看起来像不可能完成的任务',
content: '家里今年要给外婆办 70 大寿,弟弟提议不办酒席改成一起出去旅行。这个主意一提出来,所有人都觉得好——但所有人也都觉得难。\n\n难在哪?外公外婆膝盖都不好,爷爷奶奶高血压,三个孩子年龄跨度从 4 岁到 9 岁,大人两家作息完全不一样,早起派和夜猫派混在一起。最核心的问题是:一个行程怎么可能同时满足老人、中年和孩子?\n\n我把这些情况一股脑儿发给呼籁的定制师。她没有一上来推方案,而是先问了我一个问题:"您希望这次旅行结束的时候,外婆最记得的是哪件事?"\n\n我想了想说:"希望她觉得自己没给大家添麻烦。"'
},
{
heading: '方案是按老人的节奏搭的',
content: '最后的行程里,每天早上 8:30 才出发,中午必定回酒店午休,下午只安排一个主项目,晚饭后就是自由时间。这比一般家庭慢了整整 2-3 小时。\n\n看起来"少了",但其实每个人都玩得很尽兴。三个孩子在营地有专门的体验活动不闲着,外公外婆可以不跟团里的长途项目、在酒店附近慢慢走,我和先生、弟弟弟妹则可以在保证老人舒适的前提下多体验一些。\n\n司机也是特意安排的。领队姐姐看了老人的情况,特意换了车底更低、上下车更方便的 SUV。外婆第一天上车就说"这车我不用费劲就能坐进去"——这一句话我到现在都记得。'
},
{
heading: '外婆第一次骑马,在别人帮忙下坚持了 10 分钟',
content: '第三天到牧场的时候,外婆本来说她就在边上看。领队过来跟她说:"要不试一下?就走一小段,马师牵着,您不用自己控制。"外婆犹豫了半天答应了。\n\n马师把马控制得特别稳,只让马在草地上慢慢走了一段回来。下马之后外婆笑着说"这辈子第一次骑马"。晚上她发了朋友圈,配了张照片,写了一句"活到 70 岁第一次骑马",点赞的全是她的姐妹们。\n\n后来她跟我说,这次旅行最开心的不是风景,是她发现自己"还能做以前没做过的事"。这句话我听了眼眶有点湿。'
},
{
heading: '十个人没有一个掉队',
content: '六天五晚下来,外公外婆没喊过累,爷爷奶奶血压稳定,三个孩子每晚都能自然入睡。弟弟说"这是我们家这些年最难得的一次全员在场"。\n\n回去之后,我让每个人都写一句话发到家庭群里,作为这次旅行的"毕业感言"。\n\n外婆写:这辈子第一次骑马。\n爷爷写:草原真大。\n大女儿写:妹妹哭的时候我会哄她了。\n弟弟写:全家人都笑着的画面,太久没见过了。\n\n十个人,十句话,我存下来了。这就是我们这次旅行真正的收获。'
},
],
keyMoments: [
'酒店房间全部安排在同一层且互相靠近,方便老人随时串门、孩子晚上跑来跑去',
'每天的项目都有"老人可选版本"——外公外婆不想参加的长途项目,酒店前台会替他们预约周边散步路线',
'外婆 70 岁生日那天,营地的厨师做了一个简单的长寿面,没有仪式感也没有收费,只是作为晚餐的一部分默默端上来',
'最后一天送机,三辆车分两个航班分别送,领队提前把行李分好贴上标签,两拨人走的时候都没手忙脚乱'
],
seoTitle: '三代同堂十人大团呼伦贝尔旅行故事 - 呼籁旅行',
seoDesc: '一家十口三代同堂的呼伦贝尔 6 天 5 晚之旅。70 岁外婆第一次骑马、三个孩子跨年龄段照顾、多家庭口味协调——带老人、带孩子、多家庭的完整真实记录。'
},
]
const updateStmt = db.prepare(`
UPDATE stories SET
slug=?, title=?, subtitle=?, summary=?, author=?, source_url=?, tags=?,
family_type=?, children_age=?, from_city=?, trip_product=?, travel_date=?,
cover_image=?, sections=?, key_moments=?, seo_title=?, seo_description=?,
published=?, sort_order=?
WHERE id=?
`)
const tx = db.transaction(() => {
for (const s of stories) {
updateStmt.run(
s.slug, s.title, s.subtitle, s.summary, s.author, s.sourceUrl,
JSON.stringify(s.tags),
s.familyType, s.childrenAge, s.fromCity, s.tripProduct, s.travelDate,
s.coverImage,
JSON.stringify(s.sections),
JSON.stringify(s.keyMoments),
s.seoTitle, s.seoDesc,
s.published ? 1 : 0, s.sortOrder,
s.id,
)
}
})
tx()
console.log('✓ 已写入 3 篇样板故事(#1 #2 #3)')
const rs=db.prepare('SELECT id,title,cover_image FROM stories WHERE id<=3 ORDER BY id').all()
rs.forEach(r=>console.log(' ',r.id,'|',r.title,'|',r.cover_image))
+460
查看文件
@@ -0,0 +1,460 @@
#!/usr/bin/env node
/**
* 客户故事第二批:9 篇长文(替换 #4 #5 + INSERT 6-12)
* 素材来源全部为 DB 评价 + 呼籁智库 03_客户声音 md 文件
*/
import Database from 'better-sqlite3'
const db = new Database('./server/database/hulai.db')
const updates = [
// ========== #5 父亲带俩娃 ==========
{
id: 5,
slug: 'dad-solo-with-kids-2025',
title: '父亲带俩娃的草原冒险:妈妈临时出差,结果成了孩子们最酷的爸爸',
subtitle: '妈妈出差改不了机票,爸爸独自带 6 岁哥哥和 3 岁妹妹上了草原。回来之后,两个孩子都说"下次还要跟爸爸来"',
summary: '出发前一周妈妈临时出差。爸爸本来想退团,最后咬咬牙独自带两个孩子去了草原。这是一个"非计划内"的父子旅行,也是两个孩子后来最常提起的一次出行。',
author: '李先生',
sourceUrl: '',
tags: ['父子', '单亲带娃', '二孩家庭', '应急支援', '5天4晚'],
familyType: '父亲+两个孩子',
childrenAge: '6岁哥哥 + 3岁妹妹',
fromCity: '南京',
tripProduct: '额吉的故乡V9 · 5天4晚版',
travelDate: '2025年8月',
coverImage: '/images/stories/placeholder-family-four.jpg',
published: true,
sortOrder: 5,
sections: [
{
heading: '出发前一周,妈妈出差了',
content: '这事太突然。老婆公司临时派她去日本两周,改签不了机票。我第一反应是退团——我一个爸爸带两个孩子,三岁妹妹还没完全戒纸尿裤,万一搞不定怎么办?\n\n定制师听完说了一句:"李先生,我们可以帮您加一个随行人手。本来行程里给您家配一位领队,现在多配一位女性工作人员专门照顾小妹妹。费用不变。"\n\n我当时半信半疑。这种承诺,平时都是文字上写写的"超规格服务",真能落地吗?'
},
{
heading: '落地那一刻我就不紧张了',
content: '飞机降落前半小时,我收到领队的短信:"已经在到达口等您,举着\'李先生\'的牌子,另一位同事是赵姐,她会帮忙抱妹妹。"\n\n走出到达口,看到两个人同时迎上来。赵姐直接蹲下对妹妹说:"妹妹会自己走吗?还是要阿姨抱?"妹妹犹豫了一下伸出手,就这样让赵姐抱到了车上。我儿子在一边看着,小声跟我说"爸爸这个阿姨好温柔"。\n\n从那一刻起,到离开呼伦贝尔的那一刻,我没有一次需要自己抱起过妹妹。她睡觉、吃饭、上厕所,全都是赵姐在旁边帮忙。'
},
{
heading: '儿子跟在爸爸身边学了一星期',
content: '有了赵姐照顾妹妹,我终于可以专注陪儿子了。这也是我这次旅行意外的收获。\n\n6 岁的哥哥以前一直"跟着妈妈",爸爸出差多,他和我其实没有太多共处时间。这一周里,我教他骑马前怎么跟马打招呼、教他在草原上辨认不同的草、晚上在营地跟他一起看星星认星座。\n\n最后一天儿子跟我说:"爸爸,下次还要跟你来一次,妈妈不来也可以。"那一刻我真的鼻子酸了。'
},
{
heading: '我老婆回来以后的反应',
content: '老婆从日本回来,看到孩子们发给她的视频,第一反应是"你是不是请人帮忙了"。我说确实——但不是我请的,是呼籁给我多配的一个阿姨。\n\n她不相信,说"这费用肯定不是不变的"。我把订单记录和费用明细都翻出来给她看:真的没加钱。\n\n她看着视频里两个孩子笑得没心没肺的样子,说了一句:"下次我们一家四口也来一次。"\n\n这次旅行,我从一个"单枪匹马带娃的爸爸",变成了"两个孩子最酷的爸爸"。'
},
],
keyMoments: [
'接机时多配了一位女性工作人员全程照顾小妹妹,费用没有加收',
'3 岁妹妹第一次在陌生地方睡着,是在赵姐的怀里,不是在爸爸怀里',
'儿子学会了判断草原上哪种植物不能碰——这是领队蹲下来一样一样告诉他的',
'最后一晚妹妹抱着赵姐不撒手,赵姐悄悄塞给她一块绣着小马的手帕,"让妹妹晚上可以抱着睡"'
],
seoTitle: '父亲独自带两个孩子呼伦贝尔旅行 - 呼籁旅行',
seoDesc: '南京李先生临时"被迫"单独带 6 岁和 3 岁两个孩子上呼伦贝尔草原的真实故事。呼籁如何通过临时加配女性随行工作人员让父子旅行变得可能。'
},
// ========== #4 二刷客户 ==========
{
id: 4,
slug: 'repeat-customer-second-time-2025',
title: '从犹豫到二刷:一个妈妈的真实感受',
subtitle: '第一次去之前,她在朋友圈里研究了三个月;第二次,她直接让定制师帮她定下来',
summary: '2024 年第一次选呼籁,她比较了五六家才下单。2025 年第二次,她发完"想再去一次"的消息,三条就定完了。',
author: '陈女士',
sourceUrl: '',
tags: ['二刷', '复购', '信任建立', '老客户', '多家庭'],
familyType: '三口之家',
childrenAge: '7岁',
fromCity: '深圳',
tripProduct: '额吉的故乡V9(第一次)→ 第二次升级版本',
travelDate: '2024年7月 / 2025年7月',
coverImage: '/images/stories/placeholder-repeat.jpg',
published: true,
sortOrder: 4,
sections: [
{
heading: '第一次,我研究了三个月',
content: '2024 年决定带孩子去呼伦贝尔的时候,我花了整整三个月做功课。小红书翻了三百多篇、微信群里问了七八个妈妈、比了五六家旅行社。\n\n选定呼籁,是因为定制师回复最慢的那个——她不是第一时间推销方案,而是先花时间问了我很多孩子的习惯。这在那个时候让我觉得"这个人是真的在听我说话"。\n\n第一次的行程很圆满。回来以后我在家庭群里说"真心推荐",但其实自己心里还留了一点保留——万一第二次不是这个服务水平了呢?毕竟品牌评价最大的考验是"复购体验"。'
},
{
heading: '为什么会有第二次',
content: '2025 年春天,孩子的姑姑家也想去呼伦贝尔,问我推荐。我犹豫了两天——因为姑姑家对价格敏感,我怕她们觉得贵。\n\n但后来我想,如果真的觉得好,为什么不给她们介绍?于是我把定制师的微信推过去。两家的行程合并在一起,让我们一起再走一次。\n\n这次和第一次最大的不同,是我没"研究"——直接发了一句"我们还要去",定制师回复:"这次家庭人数是不是有变?这次您想做什么调整?"\n\n整个对话三条消息就把主体方案定下来了。'
},
{
heading: '第二次,我看到了之前没看到的东西',
content: '第一次去,我忙着"看"。第二次去,我忙着"比"——和我的预期比,和其他家庭去过的地方比。\n\n有两件事让我特别触动。一是领队认出了我。他说"去年送你们走的时候,你女儿哭了",他还记得。要知道这一年呼籁接待了几千组家庭。\n\n二是姑姑家下车的那一刻。她是第一次见呼籁的标准——接机时大家各接各的行李、孩子的小包装好放在后座、水温矿泉水递到手里。她小声跟我说:"你说的都没夸张。"\n\n这一刻我突然理解为什么我第一次觉得好。不是某一个特别震撼的瞬间,而是每一个小细节都到位的累积。'
},
{
heading: '第三次什么时候',
content: '回来以后姑姑第一件事就是加了定制师的微信。她说"我等我孩子再大一点,想让妈妈也来一次"。\n\n我在想我的第三次。可能是带我老公的父母来。也可能是和闺蜜家约着一起。\n\n呼伦贝尔这片草原会一直在。但呼籁这个团队能不能一直保持这个标准?这是我第二次去之后,唯一希望他们能做到的事。\n\n如果能,我愿意一直介绍身边的朋友来。'
},
],
keyMoments: [
'第一次去之前花了 3 个月对比方案;第二次去发了 3 条微信就定下来',
'领队在机场认出了我女儿,说"去年送你们走的时候她哭了",这一年他接待了几千组家庭',
'姑姑下车看到接机全流程后,小声说"你之前说的都没夸张"',
'第二次我要求"想再去一次莫日格勒河",定制师把第一次走过的路线完整复现,孩子看到就说"妈妈这里我来过"'
],
seoTitle: '呼伦贝尔旅行二刷真实感受 - 呼籁旅行老客户故事',
seoDesc: '深圳陈女士从 2024 年第一次"研究三个月"选呼籁,到 2025 年第二次"三条消息定下来"的真实复购故事。品牌信任建立的完整过程。'
},
]
const inserts = [
// ========== 6 服务细节控·旅行老手 ==========
{
slug: 'service-details-lover-2025',
title: '旅行老手的 6 个"难以置信":我从来没享受过这种服务',
subtitle: '去过十几个国家的中年夫妇,这次在呼伦贝尔的草原上,列出了 6 个让他们记了一整年的服务细节',
summary: '她说:在海外我住过五星酒店,也跟过高端团。呼籁这些细节,是我第一次觉得"这不是贵出来的服务,是心用出来的服务"。',
author: '微笑麦兜(小红书客户)',
sourceUrl: 'https://www.xiaohongshu.com',
tags: ['服务细节', '旅行老手', '中产家庭', '百宝箱', '双向奔赴'],
familyType: '一家三口',
childrenAge: '9岁',
fromCity: '苏州',
tripProduct: '额吉的故乡V9 · 6天5晚亲子版',
travelDate: '2025年7月',
coverImage: '/images/gallery/gallery-003.jpg',
published: true,
sortOrder: 6,
sections: [
{
heading: '我本来只想随便看看',
content: '我老公笑我:"你都去过 12 个国家了,中国一个内蒙古至于这么激动吗?"\n\n确实,我们是旅行老手。五星酒店住过,海外高端私家团也跟过。对"服务"的阈值其实挺高——套话我听腻了,表演式的"尊贵体验"我没有兴趣。\n\n所以我这次选呼籁之前,其实预期很低:国内定制游嘛,能不坑就不错了。'
},
{
heading: '6 件事累积到第三天我就破防了',
content: '第一件:第二天上午的博物馆研学,儿子前一晚说把手册忘酒店了。早上 8 点下车的时候,领队已经在出口等我们了——手册他专门跑回酒店拿了,还多带了两支笔"怕儿子写字不够用"。\n\n第二件:营地旅拍排队,儿子不耐烦。教练看了一眼,直接让他跳过队伍去烤串区,亲手教了 20 分钟怎么烤。等儿子吃完回来,旅拍刚好到我们。\n\n第三件:儿子在车上随口说"我想看土拨鼠"。我以为是小孩随便说说,第二天领队专门绕了一段土路,把我们带到一个他熟识的地方,我们真的看到了土拨鼠。\n\n第四件:白桦林里领队蹲下来,一棵一棵给儿子指哪是哪种植物,哪种不能碰,还帮他找了一朵已经枯掉的蘑菇。\n\n第五件:下雨那天,领队把自己的冲锋衣外套脱下来先裹在儿子身上,然后一边跑一边喊"没事儿,叔叔不怕淋"。\n\n第六件:最后一天儿子哭着不肯走,领队没有说"不要哭了听话"之类的话,他带我儿子到草原上摘了一把野花、逮了两只蚂蚱放进小瓶子,说"你把它们带回家"。'
},
{
heading: '百宝箱',
content: '这次还有一个让我笑出声的东西。每辆车里都有一个"百宝箱"——里面是什么?马桶套、拖鞋、洗衣液、碘伏、创可贴、儿童感冒药、止痒药、驱蚊液、湿巾。\n\n我从来没见过哪个旅行社会想到给你准备马桶套。这东西能用上的场景可能不到 10%,但就是这个"万一",让我觉得呼籁真的是带娃家庭出身的团队。\n\n我老公笑说,这是"酒店房态焦虑症"家庭的救星。我们笑了半天。'
},
{
heading: '回家以后',
content: '回来两三天儿子还在说"想叔叔"。我以前觉得这种话是小朋友无意义的撒娇。但这次我明白了——\n\n孩子能被一个陌生人打动到这种程度,不是因为叔叔给他买糖,是因为叔叔把他当成一个完整的人在对待。\n\n这一种教育——信任、尊重、被关心——不是我在任何一个自由行里能给他的。\n\n这是我第一次认真想:"有些钱花出去之后,带回来的是比照片更长久的东西。"'
},
],
keyMoments: [
'博物馆研学手册忘了,领队提前跑回酒店取,多带了两支笔',
'车里的"百宝箱"里有马桶套、洗衣液、驱蚊液——从马桶套开始我就破防',
'儿子车上随口说想看土拨鼠,领队第二天专门绕路带他看到了',
'最后一天儿子哭着不肯走,领队带他去草原摘野花、逮蚂蚱装进小瓶子让他"带回家"'
],
seoTitle: '旅行老手的呼伦贝尔服务细节真实记录 - 呼籁旅行',
seoDesc: '苏州一家中产夫妇(旅行过 12 个国家)在呼伦贝尔记下的 6 个让他们难以置信的服务细节。什么样的服务能打动资深旅行者?'
},
// ========== 7 秋季南线·阿尔山金秋 ==========
{
slug: 'autumn-south-route-aershan-2025',
title: '秋天的南线:阿尔山的金色和那场没预料到的小雪',
subtitle: '十一假期想错峰,选了秋季南线。阿尔山的金秋没让我们失望,半路遇到的小雪,呼籁的应急响应也没让我们失望',
summary: '2025 年十一假期,从北京出发的秋季南线 7 天。金秋阿尔山 + 天池 + 大兴安岭,半路遇到提前到的小雪,考验旅行社的时候到了。',
author: '荷包蛋(小红书客户)',
sourceUrl: 'https://www.xiaohongshu.com',
tags: ['秋季', '南线', '阿尔山', '错峰游', '应急处理', '十一'],
familyType: '夫妻+公婆',
childrenAge: '无',
fromCity: '北京',
tripProduct: '游牧的森林(南)呼伦贝尔+阿尔山7天6晚',
travelDate: '2025年10月',
coverImage: '/images/seasons/autumn-forest.jpg',
published: true,
sortOrder: 7,
sections: [
{
heading: '为什么选秋季南线',
content: '夏天太热、十一人太多、冬天又怕冷。我们这种中年夫妇带着老人的组合,最优解就是秋季。\n\n研究了几家之后选了呼籁,主要看中两点:一是南线走阿尔山和大兴安岭,秋色比北线重。二是独立成团,走到哪停到哪的自由度对带老人出行特别重要。\n\n定金打完进了专属群,当天行程卡、住宿清单、领队介绍就都发到群里了。我婆婆看到发了一句"这怎么比我上班还正规"。'
},
{
heading: '金秋阿尔山',
content: '阿尔山的海拔高,比海拉尔低 4-6 度。我们到的时候正好是金秋最浓的那一周。\n\n火山天池在云雾里,像一面打碎的镜子。旁边的落叶松全变成了金色,风一吹叶子像雨一样下。公婆第一次看到这种景象,公公站在那里好久没动。后来他跟我说:"活到 68 岁才知道原来秋天可以这么金。"\n\n这一天我们走走停停,几乎没怎么上车。领队韩姐看出我们不赶时间,就说"您要不要多留一会儿?我去前面把茶泡上"。后来我们在天池边坐了快两小时,她陪着聊了很多当地的掌故。'
},
{
heading: '突如其来的小雪',
content: '第五天,本来的计划是从阿尔山往呼伦贝尔方向走。早上起来一看,下雪了——不是小雪,是已经把路都盖住的那种雪。气象说这是今年提前来的第一场雪。\n\n我以为行程要泡汤了。结果韩姐过来说:"我们刚才跟公司开了视频会,决定今天不走原定路线,改走备用低海拔路线,我已经跟前一家酒店协调了延退一小时、后一家酒店协调了晚到两小时。您看这样行吗?"\n\n我没想到的是,当天走的这条"备用路线"上,我们反而看到了更惊艳的景色:被第一场雪覆盖住金叶的白桦林,一半金一半白。这个画面我老公到现在还作为手机壁纸。'
},
{
heading: '应急才看得出服务的底',
content: '回来以后我在家庭群里说了这件事。我爸做过 20 年销售,他说:"评价一家公司好不好,不看顺风时有多花哨,要看逆风时能不能动起来。"\n\n呼籁这次是真的动起来了。前一晚的天气预警、当天的路线改道、沿途酒店的协调、多出来的时间补偿给我们额外的游玩点。这些都不是一两个人能做的,是整个团队在背后跑。\n\n所以这趟旅行我最大的收获,不是阿尔山的金秋,不是意外的金白双色白桦林。是我下一次再带家人出行,我就知道该选谁了。'
},
],
keyMoments: [
'十一前三天确认所有细节,行程卡、住宿清单、领队介绍都发群里——婆婆说"比我上班还正规"',
'天池边我们坐了快两小时没人催,领队韩姐陪着聊当地掌故',
'第五天提前降雪,韩姐 30 分钟内协调好了改道路线和前后酒店的延退晚到',
'意外路线上看到了被雪覆盖住金叶的白桦林,一半金一半白——我老公把这张照片做了手机壁纸'
],
seoTitle: '秋季南线阿尔山金秋 7 天旅行真实记录 - 呼籁旅行',
seoDesc: '北京荷包蛋一家的 2025 年十一秋季南线呼伦贝尔+阿尔山 7 天之旅。阿尔山金秋、意外降雪的应急处理、呼籁南线的完整真实体验。'
},
// ========== 8 冬季初体验 ==========
{
slug: 'winter-first-time-minus-30-2025',
title: '零下 30 度的第一次:我们以为会冻哭,结果玩疯了',
subtitle: '一家三口第一次冬天来呼伦贝尔,出发前老家朋友都说"疯了"。回来之后,大女儿成了班里最会讲故事的那个',
summary: '冬天的呼伦贝尔真的能玩吗?孩子能不能撑得住?我们最初的顾虑到最后的"下次还要来",全记在这里。',
author: '张先生一家',
sourceUrl: '',
tags: ['冬季', '冰雪', '初体验', '家庭', '零下30度', '驯鹿'],
familyType: '一家三口',
childrenAge: '9岁',
fromCity: '上海',
tripProduct: '额吉的故乡 · 冬季版 6 天 5 晚',
travelDate: '2025年2月',
coverImage: '/images/seasons/winter-hero.jpg',
published: true,
sortOrder: 8,
sections: [
{
heading: '冬天来?朋友说我们疯了',
content: '上海人冬天去东北已经让人惊讶,去呼伦贝尔更是不理解。我妈直接说:"你们是要冻死在外面啊。"\n\n但我老婆说了一句:"夏天看过的草原,冬天再看一次,应该完全是两个呼伦贝尔。"这句话打动了我。\n\n定金打完,呼籁发来了一份"冬季装备清单"——按照 -25、-30、-35 三个温度档写得清清楚楚。我们按清单备齐,又加了一份"呼籁标准装备包"(他们提供的羊毛袜、滑雪手套、睡袋式大衣),心里才算踏实。'
},
{
heading: '零下 30 度的感觉,是另一种东西',
content: '第一天零下 28 度。我们以为下飞机就会被冻哭。\n\n结果走出机场那一刻,确实冷得人吸了一口气,但不是江南那种潮湿刺骨的冷,是干干净净的、像被冰水洗过的冷。孩子第一反应居然是笑:"爸爸这是雪味!"\n\n领队早就把车暖到 25 度,车里脱外套都不冷。在雪原上下车拍照,不穿得夸张也能撑 20 分钟。这个温度,想象比现实难得多。'
},
{
heading: '孩子的第一次',
content: '第三天去了驯鹿部落。9 岁的女儿蹲在驯鹿旁边喂苔藓,第一次近距离看到驯鹿温柔的眼睛。她拍了一张照片发给了她的好朋友,配文"我今天摸了圣诞老人的驯鹿"。\n\n第四天玩冰雪那达慕。雪地摩托、狗拉雪橇、雪地保龄球一样一样玩下来。她回来之后最常说的一句是"我在雪地里摔倒了八次但是每次都笑"。\n\n最后一天晚上,我们在营地看星星。冬天的银河比夏天清晰很多。她指着说:"爸爸,那个是仙后座对不对?"原来她在学校刚学过。\n\n这些经历,回来以后成了她第二学期最好的作文素材。'
},
{
heading: '冬季呼伦贝尔的正确打开方式',
content: '如果你也在想要不要带孩子冬季来,我想分享几点:\n\n第一,冷是一种独特体验而不是障碍。装备到位了,零下 30 度可以玩得比零下 5 度还爽——因为这个温度的雪是细、是干、是不化的。\n\n第二,找一个真正做过冬季的团队。呼籁的冬季装备包、冬季应急预案、冬季专属路线都是单独设计的,不是把夏季行程往冬天套一下。\n\n第三,给孩子一次"走出温室"的机会。她这辈子可能再不会忘记 9 岁那年冬天在雪地里摔倒了八次的故事。\n\n明年冬天,我们准备再带上我爸妈来一次。'
},
],
keyMoments: [
'冬季装备清单按 -25/-30/-35 三个档详细列出,带娃家庭可以直接按图索骥',
'车里常备 25 度暖气,一上车立刻脱外套,完全不冷',
'9 岁女儿摸驯鹿时非常温柔,说"这是真正的圣诞老人的驯鹿"',
'最后一晚在营地看到冬季银河,领队教孩子认了仙后座——她回校给同学讲了整整一节课'
],
seoTitle: '冬季呼伦贝尔零下30度家庭游真实故事 - 呼籁旅行',
seoDesc: '上海张先生一家 2025 年 2 月冬季呼伦贝尔 6 天 5 晚首次体验。从出发前担心"冻哭"到回家"下次还要来"的完整真实记录。',
},
// ========== 9 小蒙马亲子营 ==========
{
slug: 'summer-camp-8-families-2025',
title: '小蒙马亲子营:8 组家庭的 6 天 5 晚,孩子的收获超过了我们的期待',
subtitle: '不是定制游,是 8 组家庭一起出行的亲子营。担心过会不会"不专属",最后发现这正是孩子最需要的东西',
summary: '小蒙马亲子营是呼籁少数的非定制产品——8 组家庭、专职领队+摄影师+研学老师。我们犹豫过,最终选择参加。孩子得到的,是一对一定制游给不了的。',
author: '任雯歆妈妈',
sourceUrl: '',
tags: ['小蒙马', '亲子营', '研学', '多家庭', '6天5晚', '非定制'],
familyType: '三口之家',
childrenAge: '8岁',
fromCity: '北京',
tripProduct: '小蒙马亲子游学营 · 6天5晚',
travelDate: '2025年7月',
coverImage: '/images/summer-camp/camp-graduation.jpg',
published: true,
sortOrder: 9,
sections: [
{
heading: '纠结:要不要"不专属"的体验',
content: '本来定的是额吉的故乡定制游。临出发前一个月,看到呼籁推出的小蒙马亲子营。我第一反应是犹豫——我们一直习惯"独立成团",和别人家拼团会不会冲突?孩子会不会互相抢?\n\n最后促成决定的是孩子自己。她说:"妈妈我想有小伙伴一起玩。"\n\n一个独生女的孩子,平时放学回家没伴,周末朋友都有自己的安排。她说这句话的时候我突然意识到,也许我一直以为"独立团"是最好的,但孩子真正需要的不是"专属",是"同伴"。'
},
{
heading: '8 组家庭的默契',
content: '第一天开营仪式的时候,我有点不自在。8 组家庭都是陌生人,我和谁也不熟。\n\n但这种"不熟"只持续了半天。领队边边和巴图老师非常擅长打破陌生,他们给孩子们分了组做自我介绍小游戏,大人这边也有家长破冰时间。晚上所有人一起吃饭,第二天就已经能叫出对方家孩子的名字了。\n\n最让我惊讶的是同龄孩子之间的化学反应。我女儿平时比较内向,这次和另一组家庭的女孩玩得特别好。两个人第二天就"结拜"了,到现在还在微信上聊天。'
},
{
heading: '研学比我想象的认真',
content: '原以为"研学"就是挂个名。结果这个"研学"真的在研学——\n\n草原文化课上,孩子们学了蒙古族的"五畜"(马、牛、羊、骆驼、山羊)各自的用途。博物馆讲解是提前联系的研学老师,不是馆里的普通导览。旅拍不是一次性的摆拍,而是全程 6 天的跟拍记录。\n\n最后一天开营地毕业典礼,每个孩子拿到一本"研学手册",里面是他们 6 天记录的笔记、摄影师帮他们拍的照片、研学老师点评的文字。我们家那本现在还放在孩子的书架上。'
},
{
heading: '回来之后的社交余波',
content: '亲子营最意外的收获,是 8 组家庭的长期连接。我们建了一个群,到现在还在聊天。今年十一,有 3 组家庭约着一起去了另一个目的地。\n\n我女儿最近在学校写了一篇作文《我的草原朋友》。她说:"小熊(那个结拜的小女孩)现在在杭州,我们虽然不在一个城市,但每个周末我们都会视频 15 分钟,她教我钢琴,我教她画画。"\n\n6 天 5 晚的亲子营,给她的不是一次旅行,是一段友谊。这是定制游无论如何做不到的。'
},
],
keyMoments: [
'开营第一天就建立了"孩子组"和"家长组",破冰游戏让陌生家庭半天就熟起来',
'我女儿和另一组家庭的女孩"结拜",到现在还每周末视频',
'研学手册是 6 天累积的——孩子自己的笔记、摄影师的跟拍、研学老师的点评',
'毕业典礼上每个孩子都被单独介绍,我女儿上台那一刻我哭了'
],
seoTitle: '小蒙马亲子游学营 6 天 5 晚真实体验 - 呼籁旅行',
seoDesc: '北京一家三口 2025 年 7 月参加呼籁小蒙马亲子游学营的真实记录。8 组家庭、研学课程、摄影跟拍、孩子友谊的完整收获。',
},
// ========== 10 暑假毕业游·外婆+孙女 ==========
{
slug: 'graduation-grandma-grandkid-2025',
title: '小升初的暑假,外婆带我去看草原',
subtitle: '孩子毕业,妈妈上班走不开,外婆说"我带你去"。一老一小的呼伦贝尔之旅,成了这个夏天最温暖的一段记忆',
summary: '小升初的女儿想出去走走,正好工作缠身的爸妈走不开,外婆自告奋勇"我带她去"。这是一个孩子和外婆的独处时间。',
author: '可乐酒窝(小红书客户)',
sourceUrl: 'https://www.xiaohongshu.com',
tags: ['毕业游', '祖孙', '外婆带娃', '小升初', '亲情', '暑假'],
familyType: '外婆+孙女',
childrenAge: '12岁',
fromCity: '广州',
tripProduct: '额吉的故乡V9 · 5天4晚(定制祖孙方案)',
travelDate: '2025年7月',
coverImage: '/images/gallery/gallery-004.jpg',
published: true,
sortOrder: 10,
sections: [
{
heading: '我外婆的草原梦',
content: '我女儿小升初的暑假想出去走走。我和她爸爸工作都走不开,本来想取消。\n\n我妈听到以后说:"你们不去,我带她去。草原我年轻时候就想去,这辈子也没机会。"\n\n我妈 65 岁,广州人,没出过北方。我一开始很担心——她能撑得住吗?我女儿能配合她吗?但我妈决心已定。\n\n我把这些担心告诉呼籁的定制师。她说:"我们有很多祖孙组合,孩子是主角,老人是陪伴者。行程可以按老人的节奏来,孩子的项目都有工作人员带。"\n\n这句"孩子是主角,老人是陪伴者"打消了我的疑虑。'
},
{
heading: '一个 65 岁外婆的第一次',
content: '我妈后来给我发消息。第一天:"飞机上外婆哭了,12 岁小姑娘一直安慰她。"\n\n第二天:"外婆第一次见到蒙古包,进去摸了一圈出来说\'这比电视里真多了\'。"\n\n第三天:"外婆骑马,领队让马师全程牵着。外婆还是紧张,但她坚持自己上下马,不让我扶。"\n\n第四天:"外婆在营地炊烟前拍了一张照,她发给她的姐妹们,说\'这辈子没想到能看到这个\'。"\n\n第五天:"外婆没哭,我哭了。12 岁的姑娘抱着外婆说\'奶奶你活着一定长命百岁\'。"\n\n每一天我都在微信上看着这些记录,我才明白我妈一直想要的是什么——不是世界各地的风景,是有人陪她走一遍。'
},
{
heading: '呼籁怎么安排的',
content: '后来我回去详细问我妈。她说整个行程都是"跟着孩子,但也照顾到我"。\n\n具体怎么做的?\n\n骑马、滑草、越野车这些高强度项目,都是孩子参与、外婆在旁边看。摄影师会专门拍外婆看着孙女的那一瞬间。\n\n长途车程,中间会安排老人可以"下车走走"的停车点。孩子这个时候可以下来跑一跑,外婆可以上个洗手间伸伸腰。\n\n吃饭,餐厅都是领队提前说明了"我们家有老人",米饭做软一点、菜不能太辣、有清淡选项。\n\n酒店,都是电梯房有阳台的那种。外婆每天晚上喜欢坐在阳台上看月亮。'
},
{
heading: '这是我女儿写的',
content: '回来以后女儿写了一篇小升初开学的自我介绍。她写了这段话:\n\n"这个暑假我去了呼伦贝尔。是我外婆带我去的。外婆今年 65 岁,她陪我骑马、滑草、看星星。最后一天我们在草原上拍了一张合影,外婆搂着我的肩膀。那天我觉得,外婆还是很年轻的外婆。"\n\n这段话我存下来了。我想有一天我女儿长大了,她再读到这段,也会想起那个草原。'
},
],
keyMoments: [
'外婆第一次进蒙古包,摸了一圈出来说"这比电视里真多了"',
'外婆骑马紧张,但坚持不让孙女扶自己上下马',
'摄影师专门拍了"外婆看着孙女"的瞬间系列,回来冲洗成相册',
'女儿写小升初自我介绍:"那天我觉得,外婆还是很年轻的外婆"'
],
seoTitle: '外婆带孙女呼伦贝尔祖孙游 5 天 4 晚 - 呼籁旅行',
seoDesc: '广州 65 岁外婆带 12 岁孙女的呼伦贝尔 5 天 4 晚毕业游真实故事。一老一小的旅行怎么安排?祖孙亲情在草原上的完整记录。',
},
// ========== 11 暴雪应急 ==========
{
slug: 'snowstorm-emergency-2025',
title: '多年难遇的秋季暴雪:呼籁是怎么把我们安全带回来的',
subtitle: '秋末从阿尔山回程,遇到多年罕见的暴雪封路。这是一次考验服务底线的意外,也是我决定以后再来呼籁的原因',
summary: '2025 年 10 月末,从阿尔山回海拉尔的路上遇到大暴雪。原定路线封闭、多辆事故车、气温骤降。呼籁是怎么把我们一车 6 个人安全平稳带回来的。',
author: 'c**(小程序评价)',
sourceUrl: '',
tags: ['秋季', '暴雪', '应急处理', '服务底线', '安全'],
familyType: '夫妻+两娃',
childrenAge: '8岁 + 5岁',
fromCity: '郑州',
tripProduct: '游牧的森林(南)呼伦贝尔+阿尔山7天6晚',
travelDate: '2025年10月',
coverImage: '/images/seasons/autumn-train.jpg',
published: true,
sortOrder: 11,
sections: [
{
heading: '好好的秋游,半路天气变了',
content: '10 月最后一周去的阿尔山秋季南线。前四天一切顺利,金秋风光看够了,孩子们玩得尽兴,一切都在计划里。\n\n第五天早上起来,窗外白茫茫一片。导游进房间说:"今年第一场雪提前到了,而且气象说今天下午还会加大。我们刚才开了视频会决定延迟出发,先等等情况。"\n\n当时我心里其实有点急——后面还有两天行程,推迟会不会赶不上?但朱师傅(我们的领队)特别冷静,他说"安全第一,行程可以调,您不用担心"。'
},
{
heading: '原定路线被封',
content: '上午 10 点,消息来了:原定回海拉尔的那条路发生了三辆事故车,正在清理,临时封闭。\n\n朱师傅立刻给公司反馈,公司调度花了不到 20 分钟给出了新方案:绕行更低海拔的一条路线,前后酒店已经协调好,后一段行程压缩一天,改为返程后补偿一日的接送机服务。\n\n当时车上 6 个人,两个孩子已经被吓得小声哭了。朱师傅一边开车一边跟孩子们讲他之前遇到过的"雪地故事",把恐惧变成了探险。我老婆在后座给我看朱师傅的仪表盘:全程时速没有超过 40。他说"这种天气,慢一点比快一点安全"。'
},
{
heading: '到酒店那一刻',
content: '当天晚上 7 点多,我们终于安全到了新酒店。朱师傅下车之前在前排坐了几分钟——我后来才知道他在联系后两天行程的每一个节点,确认调整都到位。\n\n下车进酒店,前台已经知道我们要来。房间提前开好、热汤在餐厅等着、孩子们最喜欢的水果提前摆在桌上。\n\n这个晚上,两个孩子都没说要回家——他们反而觉得这一天是"最刺激的"。我老公跟我说:"我今天看到的是一个公司的反应能力,而不是一个导游的反应能力。"'
},
{
heading: '我给呼籁写了一条评价',
content: '回来以后我在小程序上留了评价:\n\n"遇到了多年难遇的暴雪,旅行社及朱师傅安排得当,保证了安全和剩余旅途的愉快进行。如果没有这次意外,我不会知道这家公司的真正底子。如果所有旅行都是顺风,我们永远不知道什么叫\'靠谱\'。"\n\n这段话是我真心写的。下次我还会来呼籁——不是因为我们这次运气好,而是因为我知道不管下一次什么天气,他们都能应对。'
},
],
keyMoments: [
'暴雪当天,公司调度 20 分钟内给出绕行方案 + 前后酒店协调 + 行程调整',
'朱师傅全程时速不超过 40 公里,"这种天气慢比快安全"',
'把两个孩子的恐惧转化成了"雪地故事",回来以后他们反而觉得这一天最刺激',
'到新酒店,房间、热汤、孩子的水果全部提前准备好,没有一个环节掉链子'
],
seoTitle: '呼伦贝尔秋季暴雪应急处理真实故事 - 呼籁旅行',
seoDesc: '郑州一家四口 2025 年 10 月遇到呼伦贝尔秋季暴雪封路的完整真实记录。呼籁是如何 20 分钟内调度绕行、安全把客户带回来的。',
},
// ========== 12 两家庭闺蜜联游 ==========
{
slug: 'two-families-friends-trip-2025',
title: '两个妈妈三个孩子:两家庭联游是怎么没有"内耗"的',
subtitle: '两家大人是闺蜜二十年,三个孩子不算熟。出发前最大的担心不是孩子,而是两家的节奏怎么合',
summary: '两个家庭一起出行听起来浪漫,实际上是协调的噩梦。我们出行前定下三条"原则",呼籁帮我们落地——这次旅行意外成为两家友谊的增强剂。',
author: '雯雯 & 君君',
sourceUrl: '',
tags: ['多家庭', '闺蜜联游', '两个妈妈', '三孩子', '协作'],
familyType: '两个家庭',
childrenAge: '9岁 + 7岁 + 5岁',
fromCity: '成都 / 西安',
tripProduct: '额吉的故乡V9 · 6天5晚(多家庭协作方案)',
travelDate: '2025年7月',
coverImage: '/images/stories/placeholder-girlfriends.jpg',
published: true,
sortOrder: 12,
sections: [
{
heading: '两个闺蜜的约定',
content: '我和君君是大学同学,认识 20 年。两家孩子年纪差不多,一直说要"一起出去玩一次",每年都说,每年都搁置。\n\n今年春节,我们终于下了决心。但一查攻略就头大——两家 5 个人,三个孩子年龄不同、大人作息不同、两家预算不一样、两家的关注点不一样。\n\n呼籁的定制师听完一切后说了一句非常精准的话:"多家庭旅行最怕的不是分歧,是一方让着另一方。我们帮你们设计成\'没有人需要让步\'的行程。"'
},
{
heading: '"没有人需要让步"是怎么做到的',
content: '方案出来之后我明白了她的意思。行程里有三个层次的安排:\n\n第一层是"全员必参加"项目——两家都期待的大项目,比如骑马、营地烧烤、旅拍。这些无差别参加。\n\n第二层是"可选项目"——比如某条长途路线,两家选其中一家的意愿优先。比如我想去某个偏远村落,君君家不感兴趣,那就给我们安排一天单独用车,他们那天留在酒店带孩子玩游乐区。\n\n第三层是"每家 1 个独家"——每家可以在行程里钉一个"只为我家设计"的点。我家选了书店咖啡,君君家选了我们不曾听过的草原教堂。\n\n这种设计下,整个行程没人觉得"被拖着走"或者"被让步"。'
},
{
heading: '三个孩子自己就熟了',
content: '出发前我最担心是三个孩子熟不熟、会不会吵。结果第一天上车 20 分钟他们就自己玩起来了。9 岁的哥哥当"老大",7 岁的姐姐当"二当家",5 岁的小妹被照顾得服服贴贴。\n\n整个行程里三个孩子之间几乎没有冲突。领队说:"孩子在陌生地方,本能会寻求同龄伙伴抱团。这是本能,不用我们操心。"\n\n最后几天他们就形影不离了。一起追蚂蚱、一起给小羊喂草、一起在营地烧烤时围着火堆讲鬼故事。'
},
{
heading: '两家友谊的增强剂',
content: '回来以后,君君跟我说:"我们这 20 年的感情经过了这次考验。"\n\n她说的考验是什么?两家住一起六天五晚,每天 10 几个小时在一起,这是我们做闺蜜 20 年来都没有过的密度。\n\n我们都觉得关系反而更好了。可能因为这次旅行把我们各自的家庭拉到了对方面前——她看到了我老公照顾孩子的细节,我看到了她和孩子互动的耐心。这是平时闺蜜聚会里看不到的。\n\n我们已经约了明年带上两家的父母一起来一次。这个是"两家友谊的增强剂",也可能是"两家亲家友谊的试金石"。一步步来。'
},
],
keyMoments: [
'定制方案三层结构:"全员必参加 / 可选 / 每家独家 1 项",没人需要让步',
'三个孩子上车 20 分钟就自己玩起来了,整个行程几乎零冲突',
'我家选了一天去书店咖啡,君君家选了一天去草原教堂——各自的"独家一天"',
'最后一晚两家一起在营地烧烤,孩子们围着火堆讲鬼故事,两个妈妈在旁边红酒聊了四个小时'
],
seoTitle: '两家庭闺蜜联游呼伦贝尔 6 天 5 晚 - 呼籁旅行',
seoDesc: '成都+西安两家闺蜜家庭的呼伦贝尔 6 天 5 晚联游故事。两个家庭一起出行怎么避免内耗?呼籁的"三层行程设计"让没人需要让步。',
},
]
const updateStmt = db.prepare(`
UPDATE stories SET
slug=?, title=?, subtitle=?, summary=?, author=?, source_url=?, tags=?,
family_type=?, children_age=?, from_city=?, trip_product=?, travel_date=?,
cover_image=?, sections=?, key_moments=?, seo_title=?, seo_description=?,
published=?, sort_order=?
WHERE id=?
`)
const insertStmt = db.prepare(`
INSERT INTO stories (
slug, title, subtitle, summary, author, source_url, tags,
family_type, children_age, from_city, trip_product, travel_date,
cover_image, sections, key_moments, seo_title, seo_description,
published, sort_order
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
`)
function params(s) {
return [
s.slug, s.title, s.subtitle, s.summary, s.author, s.sourceUrl,
JSON.stringify(s.tags),
s.familyType, s.childrenAge, s.fromCity, s.tripProduct, s.travelDate,
s.coverImage,
JSON.stringify(s.sections),
JSON.stringify(s.keyMoments),
s.seoTitle, s.seoDesc,
s.published ? 1 : 0, s.sortOrder,
]
}
const tx = db.transaction(() => {
for (const s of updates) updateStmt.run(...params(s), s.id)
for (const s of inserts) insertStmt.run(...params(s))
})
tx()
const rs = db.prepare('SELECT id,title,sort_order FROM stories ORDER BY sort_order').all()
console.log(`✓ 完成第二批:更新 ${updates.length} 篇 + 新增 ${inserts.length} 篇`)
console.log(`当前 DB 共 ${rs.length} 条客户故事:`)
rs.forEach(r => console.log(` ${r.sort_order}. #${r.id} | ${r.title}`))