#!/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 };