| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496 |
- #!/usr/bin/env node
- /**
- * 小牛看房 — 房源智能匹配引擎 v1.0
- *
- * 用法:
- * node match-engine.js # 匹配全部客户
- * node match-engine.js --buyer buyer_001 # 匹配单个客户
- * node match-engine.js --buyer buyer_001 --top 3 # 输出 Top-3
- */
- const fs = require('fs');
- const path = require('path');
- // ============================================================
- // 一、数据加载
- // ============================================================
- function loadData() {
- const buyers = JSON.parse(
- fs.readFileSync(path.join(__dirname, 'buyers.json'), 'utf8')
- ).clients;
- const properties = JSON.parse(
- fs.readFileSync(path.join(__dirname, 'properties.json'), 'utf8')
- ).properties;
- return { buyers, properties };
- }
- // ============================================================
- // 二、五类客户权重模板(来源:经纪人访谈 + 客户画像分析)
- // ============================================================
- const WEIGHT_TEMPLATES = {
- '婚房刚需型': {
- priceAdvantage: 0.30,
- layoutMatch: 0.15,
- areaMatch: 0.10,
- decoration: 0.10,
- schoolMatch: 0.05,
- transport: 0.10,
- community: 0.05,
- floorMatch: 0.05,
- orientation: 0.05,
- surrounding: 0.05,
- },
- '学区焦虑型': {
- priceAdvantage: 0.15,
- layoutMatch: 0.10,
- areaMatch: 0.05,
- decoration: 0.05,
- schoolMatch: 0.45,
- transport: 0.10,
- community: 0.05,
- floorMatch: 0.02,
- orientation: 0.02,
- surrounding: 0.01,
- },
- '置换改善型': {
- priceAdvantage: 0.20,
- layoutMatch: 0.20,
- areaMatch: 0.20,
- decoration: 0.15,
- schoolMatch: 0.05,
- transport: 0.05,
- community: 0.10,
- floorMatch: 0.03,
- orientation: 0.02,
- surrounding: 0.00,
- },
- '隐性需求型': {
- priceAdvantage: 0.20,
- layoutMatch: 0.10,
- areaMatch: 0.10,
- decoration: 0.25,
- schoolMatch: 0.05,
- transport: 0.10,
- community: 0.10,
- floorMatch: 0.05,
- orientation: 0.03,
- surrounding: 0.02,
- },
- '刚需升级型': {
- priceAdvantage: 0.25,
- layoutMatch: 0.15,
- areaMatch: 0.15,
- decoration: 0.20,
- schoolMatch: 0.05,
- transport: 0.05,
- community: 0.05,
- floorMatch: 0.05,
- orientation: 0.03,
- surrounding: 0.02,
- },
- };
- // ============================================================
- // 三、卖点标签 → 客户类型加分映射(来源:经纪人访谈提炼)
- // ============================================================
- const HIGHLIGHT_BONUS = {
- '豪装': { '隐性需求型': 3, '刚需升级型': 3 },
- '精装': { '隐性需求型': 2, '刚需升级型': 2, '置换改善型': 1 },
- '满五唯一': { '婚房刚需型': 2 },
- '业主急售': { '婚房刚需型': 2, '置换改善型': -1 },
- '急售': { '婚房刚需型': 2, '置换改善型': -1 },
- '南北通透': { '婚房刚需型': 1, '置换改善型': 1, '刚需升级型': 1, '隐性需求型': 1, '学区焦虑型': 1 },
- '明厨明卫': { '置换改善型': 2 },
- '近地铁': { '婚房刚需型': 2 },
- '地铁房': { '婚房刚需型': 2 },
- '人车分流': { '置换改善型': 2 },
- '近公园': { '置换改善型': 1, '隐性需求型': 1 },
- '次新房': { '婚房刚需型': 1, '置换改善型': 1, '刚需升级型': 1 },
- '有车位': { '置换改善型': 1 },
- '总价低': { '婚房刚需型': 2, '刚需升级型': 1 },
- '品牌开发商': { '置换改善型': 1, '刚需升级型': 1 },
- '婚装': { '隐性需求型': 2 },
- };
- // ============================================================
- // 四、客户心理 → 房源加分(来源:七步法 + 访谈)
- // ============================================================
- function calcPsychologyBonus(buyer, property) {
- let bonus = 0;
- const style = buyer.decisionStyle;
- const type = buyer.type;
- // 对比型 — 性价比突出
- if (style === '对比型' && property.priceAdvantage >= 7) bonus += 2;
- // 谨慎型 — 无硬伤、产权清晰
- if (style === '谨慎型' && property.isFiveYearOnly && !property.isGroundFloor && !property.isTopFloor) {
- bonus += 2;
- }
- // 感性/冲动型(隐性需求、刚需升级)— 装修亮眼
- if ((style === '冲动型' || type === '隐性需求型') && ['豪装'].some(t => property.highlightTags.includes(t))) {
- bonus += 3;
- }
- if ((style === '冲动型' || type === '刚需升级型') && ['豪装', '婚装'].some(t => property.highlightTags.includes(t))) {
- bonus += 2;
- }
- // 置换改善 — 人车分流+绿化好
- if (type === '置换改善型' && property.highlightTags.includes('人车分流') && property.communityQuality >= 8) {
- bonus += 1;
- }
- return Math.min(bonus, 3);
- }
- // ============================================================
- // 五、阶段 1:硬约束过滤
- // ============================================================
- function hardFilter(buyer, properties) {
- // 反推真实预算上限
- const loanCoefficient = 180; // 简化:30年贷款系数
- const maxLoan = buyer.monthlyPaymentCapacity * loanCoefficient / 10000;
- const realBudgetMax = Math.max(
- buyer.budgetMax,
- (buyer.downPayment + maxLoan) * 0.9
- );
- return properties.filter(p => {
- // F1: 预算上限(真实上限×1.1)
- if (p.totalPrice > realBudgetMax * 1.1) return false;
- // F2: 区域不匹配
- const districtMatch = buyer.targetDistricts.some(d => p.district.startsWith(d));
- if (!districtMatch) return false;
- // F3: 房龄超标
- if (p.buildingAge > buyer.buildingAgeMax) return false;
- // F4: 必须学区
- if (buyer.schoolDistrictRequired && !p.isSchoolDistrict) return false;
- if (buyer.schoolDistrictRequired && buyer.targetSchools && !buyer.targetSchools.includes(p.schoolName)) return false;
- // F5: 排斥项
- if (buyer.resistFactors.includes('底层') && p.isGroundFloor) return false;
- if (buyer.resistFactors.includes('顶楼') && p.isTopFloor) return false;
- if (buyer.resistFactors.includes('临街') && (p.highlightTags.includes('临街') || p.communityQuality <= 3)) return false;
- // F6: 特殊需求
- if (buyer.specialRequirements.includes('车位或可租车位') && p.parking === '无') return false;
- if (buyer.specialRequirements.includes('电梯') && !p.highlightTags.includes('电梯') && p.floorLevel !== '低') {
- // 简单判断:如果是低楼层且没有电梯标签,可能无电梯
- }
- if (buyer.specialRequirements.includes('人车分流') && !p.highlightTags.includes('人车分流')) return false;
- // F7: 面积硬下限
- if (p.area < buyer.areaMin * 0.85) return false;
- return true;
- });
- }
- // ============================================================
- // 六、阶段 2:加权评分
- // ============================================================
- function scoreDimension(buyer, property, weights) {
- const scores = {};
- // D1: 价格优势
- scores.priceAdvantage = property.priceAdvantage / 10;
- // D2: 户型匹配
- const layoutExact = buyer.preferredLayouts.includes(property.layout);
- const layoutPartial = buyer.preferredLayouts.some(pl => {
- const pMatch = pl.match(/(\d+)室(\d+)厅/);
- const propMatch = property.layout.match(/(\d+)室(\d+)厅/);
- if (pMatch && propMatch) {
- const pRooms = parseInt(pMatch[1]);
- const propRooms = parseInt(propMatch[1]);
- return pRooms === propRooms; // 室数相同算部分匹配
- }
- return false;
- });
- scores.layoutMatch = layoutExact ? 1.0 : layoutPartial ? 0.6 : 0.3;
- // D3: 面积匹配
- if (property.area >= buyer.areaMin && property.area <= buyer.areaMax) {
- scores.areaMatch = 1.0;
- } else if (property.area >= buyer.areaMin * 0.85 && property.area <= buyer.areaMax * 1.15) {
- const center = (buyer.areaMin + buyer.areaMax) / 2;
- const deviation = Math.abs(property.area - center) / center;
- scores.areaMatch = Math.max(0.3, 1 - deviation * 2);
- } else {
- scores.areaMatch = 0.2;
- }
- // D4: 装修品质
- const decorationMap = { '豪装': 1.0, '精装': 0.75, '简装': 0.4, '毛坯': 0.2 };
- const requiredDecoMap = { '豪装': 1.0, '精装': 0.75, '简装': 0.4, '不限': 0.0 };
- const requiredLevel = requiredDecoMap[buyer.decorationRequirement] || 0.5;
- const actualLevel = decorationMap[property.decoration] || 0.5;
- scores.decoration = actualLevel >= requiredLevel ? 1.0 : actualLevel / Math.max(requiredLevel, 0.1);
- // D5: 学区匹配
- if (!buyer.schoolDistrictRequired) {
- scores.schoolMatch = 1.0; // 不在乎学区,不影响评分
- } else {
- if (buyer.targetSchools && buyer.targetSchools.includes(property.schoolName)) {
- scores.schoolMatch = 1.0;
- } else if (property.isSchoolDistrict) {
- scores.schoolMatch = 0.5;
- } else {
- scores.schoolMatch = 0.0;
- }
- }
- // D6: 交通便利
- scores.transport = property.transportScore / 10;
- // D7: 小区品质
- scores.community = property.communityQuality / 10;
- // D8: 楼层匹配
- const floorMap = { '低': 0, '中': 1, '高': 2 };
- const buyerFloor = floorMap[buyer.floorPreference] !== undefined ? floorMap[buyer.floorPreference] : 1;
- const propFloor = floorMap[property.floorLevel] !== undefined ? floorMap[property.floorLevel] : 1;
- const floorDiff = Math.abs(buyerFloor - propFloor);
- scores.floorMatch = floorDiff === 0 ? 1.0 : floorDiff === 1 ? 0.6 : 0.3;
- // D9: 朝向匹配
- if (buyer.preferredOrientations.includes('不限')) {
- scores.orientation = 0.8;
- } else {
- const orientExact = buyer.preferredOrientations.some(o => property.orientation.includes(o));
- const orientPartial = buyer.preferredOrientations.some(o => property.orientation.includes(o.replace('南北通透', '南').replace('南向', '南')));
- scores.orientation = orientExact ? 1.0 : orientPartial ? 0.5 : 0.2;
- }
- // D10: 周边配套
- scores.surrounding = property.surroundingScore / 10;
- // 加权汇总
- let totalScore = 0;
- let totalWeight = 0;
- for (const [key, weight] of Object.entries(weights)) {
- if (scores[key] !== undefined) {
- totalScore += scores[key] * weight;
- totalWeight += weight;
- }
- }
- return {
- totalScore: totalWeight > 0 ? (totalScore / totalWeight) * 100 : 0,
- breakdown: scores,
- };
- }
- // ============================================================
- // 七、阶段 3:智能择优(卖点 + 心理加分)
- // ============================================================
- function calcHighlightBonus(buyerType, property) {
- let bonus = 0;
- for (const tag of property.highlightTags) {
- const tagBonus = HIGHLIGHT_BONUS[tag];
- if (tagBonus && tagBonus[buyerType]) {
- bonus += tagBonus[buyerType];
- }
- }
- return Math.min(bonus, 5);
- }
- // ============================================================
- // 八、主匹配函数
- // ============================================================
- function matchBuyer(buyer, properties, topN = 5) {
- const weights = WEIGHT_TEMPLATES[buyer.type] || WEIGHT_TEMPLATES['婚房刚需型'];
- // 阶段 1: 硬约束过滤
- const candidates = hardFilter(buyer, properties);
- // 阶段 2: 加权评分
- const scored = candidates.map(p => {
- const { totalScore, breakdown } = scoreDimension(buyer, p, weights);
- return { property: p, stage2Score: totalScore, breakdown };
- });
- // 阶段 3: 智能择优
- const final = scored.map(s => {
- const highlightBonus = calcHighlightBonus(buyer.type, s.property);
- const psychBonus = calcPsychologyBonus(buyer, s.property);
- const finalScore = s.stage2Score + highlightBonus + psychBonus;
- return {
- ...s,
- highlightBonus,
- psychBonus,
- finalScore,
- level: finalScore >= 90 ? '强烈推荐' : finalScore >= 80 ? '推荐' : finalScore >= 70 ? '备选' : '不推荐',
- };
- });
- // 排序
- final.sort((a, b) => b.finalScore - a.finalScore);
- return final.slice(0, topN);
- }
- // ============================================================
- // 九、沟通策略生成
- // ============================================================
- const STRATEGY_TEMPLATES = {
- '婚房刚需型': {
- style: '财务顾问+生活规划师',
- keyScript: (p, buyer) => `这套${p.layout}总价${p.totalPrice}万,首付约${Math.round(p.totalPrice*0.2)}万,月供大概${Math.round(p.totalPrice*0.8*0.0045*10000)}元。两居的客厅够大,以后改成三居也完全没问题。`,
- precautions: ['帮客户算清"多花10万首付=未来5年不换房"这笔账', '关注女方感受,感性决策占比大'],
- followUp: '首次推荐后2天发对比分析,1周内约带看',
- },
- '学区焦虑型': {
- style: '政策专家+数据提供者',
- keyScript: (p, buyer) => `这套对口${p.schoolName},近3年划片都没有变动,入学年限满足要求,学位也未被占用。这是去年该小区划片文件和升学率数据。`,
- precautions: ['必须准备书面证据:划片文件、学位占用情况', '客户严谨较真,不要口头承诺'],
- followUp: '首次推荐后立即发学区资料,3天内约实地看房',
- },
- '置换改善型': {
- style: '全流程管家',
- keyScript: (p, buyer) => `这套${p.layout}目前在售。您的老房子预计能卖${buyer.oldHouseEstimatedValue || 120}万,这套${p.totalPrice}万,贷款${p.totalPrice - (buyer.oldHouseEstimatedValue || 120)}万左右。如果两边节奏配合好,可以实现无缝衔接。`,
- precautions: ['提供"卖+买"一体化时间线方案', '决策链长,尽量约全家人一起看'],
- followUp: '了解老房子挂牌进展,同步推送新上房源',
- },
- '隐性需求型': {
- style: '需求翻译官+引导者',
- keyScript: (p, buyer) => `不用急着定,我们先多看几套对比一下。这套装修是亮点,您看这个阳台,以后周末在这里喝喝茶,感觉很舒服的。`,
- precautions: ['同一天带看2套差异大的房源做对比', '看房后引导客户说出喜欢/不喜欢哪里'],
- followUp: '每次带看后现场复盘感受,逐步收敛需求',
- },
- '刚需升级型': {
- style: '品质推手+性价比计算器',
- keyScript: (p, buyer) => `这套虽然比您预算多了点,但多一个独立书房和主卧套间。多花10万首付,月供只多500块,未来5年不用换房。`,
- precautions: ['先推达标房源再推品质房源,形成对比', '让客户看到"价值差"而不是"价格差"'],
- followUp: '先推一套80万,再推120万,让客户自己感受差异',
- },
- };
- function generateStrategy(buyer, property) {
- const template = STRATEGY_TEMPLATES[buyer.type] || STRATEGY_TEMPLATES['婚房刚需型'];
- return {
- style: template.style,
- openingScript: template.keyScript(property, buyer),
- precautions: template.precautions,
- followUp: template.followUp,
- };
- }
- // ============================================================
- // 十、格式化输出
- // ============================================================
- // 生成坦诚缺点(来自蓝领岗位匹配的启发:坦诚 > 隐瞒)
- function generateHonestCons(property) {
- const cons = [];
- if (property.buildingAge >= 20) cons.push('房龄较老,需关注管道老化和渗水情况');
- if (property.buildingAge >= 25) cons.push('房龄超过25年,贷款年限可能受限');
- if (property.decoration === '简装') cons.push('装修简单,入住前可能需翻新');
- if (property.decoration === '毛坯') cons.push('毛坯房,需额外准备装修预算约10-15万');
- if (property.isGroundFloor) cons.push('底层,需关注防潮和隐私问题');
- if (property.isTopFloor) cons.push('顶楼,夏季较热,需关注防水');
- if (property.parking === '无') cons.push('无车位,周边停车可能不便');
- if (property.communityQuality <= 4) cons.push('小区品质一般,绿化物业配套有限');
- if (property.transportScore <= 5) cons.push('交通便利度一般,公交/地铁覆盖较少');
- if (property.surroundingScore <= 5) cons.push('周边商业配套有限,生活便利度较低');
- if (!property.isFiveYearOnly && property.buildingAge < 5) cons.push('不满五,交易税费较高');
- if (property.floorLevel === '低' && !property.isGroundFloor) cons.push('低楼层,采光可能受遮挡影响');
- if (property.floorLevel === '高' && !property.isTopFloor && property.communityQuality <= 5) cons.push('高楼层,需确认电梯运行状况');
- if (!property.isSchoolDistrict && property.district.includes('天宁区-文化宫')) cons.push('非学区房,天宁区核心学区客户需注意');
- if (property.priceDropSpace <= 5) cons.push('业主议价空间较小,谈价弹性有限');
- return cons.length > 0 ? cons : ['无明显硬伤,整体较为均衡'];
- }
- function printResults(buyer, results) {
- console.log(`\n${'='.repeat(70)}`);
- console.log(`🎯 客户:${buyer.name}(${buyer.type})`);
- console.log(` 口述预算:${buyer.surfaceBudget}万 | 首付:${buyer.downPayment}万 | 月供承受:${buyer.monthlyPaymentCapacity}元`);
- console.log(` 目标区域:${buyer.targetDistricts.join('、')}`);
- console.log(` 核心关注:${buyer.coreConcerns.join(' > ')}`);
- console.log(`${'='.repeat(70)}`);
- if (results.length === 0) {
- console.log(`\n⚠️ 无匹配房源。建议:`);
- if (buyer.budgetMax < 100) console.log(` → 预算偏低,考虑提高预算或扩大区域`);
- if (buyer.schoolDistrictRequired) console.log(` → 学区要求严格,建议扩大目标学校范围`);
- return;
- }
- results.forEach((r, i) => {
- const p = r.property;
- const s = generateStrategy(buyer, p);
- console.log(`\n${'─'.repeat(70)}`);
- console.log(`🏠 #${i + 1} [${r.level}] ${p.community} · ${p.layout} · ${p.totalPrice}万`);
- console.log(` 综合分:${r.finalScore.toFixed(1)} (基础${r.stage2Score.toFixed(1)} + 卖点${r.highlightBonus} + 心理${r.psychBonus})`);
- console.log(` 区域:${p.district} | 面积:${p.area}㎡ | 楼层:${p.floor} | 朝向:${p.orientation}`);
- console.log(` 装修:${p.decoration} | 房龄:${p.buildingAge}年 | 学区:${p.isSchoolDistrict ? p.schoolName : '无'}`);
- console.log(` 亮点:${p.highlightTags.join(' · ')}`);
- console.log(` 业主:${p.ownerSituation} | 议价空间:${p.priceDropSpace}万`);
- // 坦诚缺点(来自蓝领匹配启发)
- const cons = generateHonestCons(p);
- console.log(` ⚠️ 坦诚提醒:${cons.join(';')}`);
- // 评分分解
- const bd = r.breakdown;
- console.log(` 📊 维度分解:价格${(bd.priceAdvantage*100).toFixed(0)} | 户型${(bd.layoutMatch*100).toFixed(0)} | 面积${(bd.areaMatch*100).toFixed(0)} | 装修${(bd.decoration*100).toFixed(0)} | 交通${(bd.transport*100).toFixed(0)} | 品质${(bd.community*100).toFixed(0)} | 楼层${(bd.floorMatch*100).toFixed(0)} | 朝向${(bd.orientation*100).toFixed(0)}`);
- console.log(`\n 💬 沟通策略(${s.style}):`);
- console.log(` "${s.openingScript}"`);
- console.log(` ⚠️ 注意事项:${s.precautions.join(';')}`);
- console.log(` 📅 跟进节奏:${s.followUp}`);
- });
- console.log(`\n${'─'.repeat(70)}`);
- console.log(`📊 筛选统计:全量${30}套 → 硬约束过滤后${results.length > 0 ? '匹配' : '0'}套 → Top-${results.length}`);
- console.log(`${'='.repeat(70)}\n`);
- }
- // ============================================================
- // 十一、入口
- // ============================================================
- function main() {
- const { buyers, properties } = loadData();
- // 命令行参数
- const args = process.argv.slice(2);
- const buyerId = args.includes('--buyer') ? args[args.indexOf('--buyer') + 1] : null;
- const topIdx = args.includes('--top') ? args.indexOf('--top') : -1;
- const topN = topIdx >= 0 ? parseInt(args[topIdx + 1]) || 5 : 5;
- const targets = buyerId
- ? buyers.filter(b => b.id === buyerId)
- : buyers;
- if (targets.length === 0) {
- console.error(`❌ 未找到客户 ${buyerId}`);
- console.error(` 可用客户ID:${buyers.map(b => b.id).join(', ')}`);
- process.exit(1);
- }
- targets.forEach(buyer => {
- const results = matchBuyer(buyer, properties, topN);
- printResults(buyer, results);
- });
- }
- if (require.main === module) {
- main();
- }
- // 导出供外部调用
- module.exports = { matchBuyer, generateStrategy, hardFilter, WEIGHT_TEMPLATES, STRATEGY_TEMPLATES };
|