diff --git a/scripts/generate-nanning-knowledge-seed.js b/scripts/generate-nanning-knowledge-seed.js
new file mode 100644
index 0000000..32bc23b
--- /dev/null
+++ b/scripts/generate-nanning-knowledge-seed.js
@@ -0,0 +1,546 @@
+const fs = require('fs');
+const path = require('path');
+
+const projectRoot = path.resolve(__dirname, '..');
+const sqlDirectory = path.join(projectRoot, 'src', 'main', 'resources', 'sql');
+const dataPath = path.join(sqlDirectory, '南宁房产知识库结构化表格.json');
+const seedPath = path.join(sqlDirectory, 'house_knowledge_seed.sql');
+const startMarker = '-- NANNING_STRUCTURED_DATA_START';
+const endMarker = '-- NANNING_STRUCTURED_DATA_END';
+const sourceNote = '本地业务资料《南宁房产知识库结构化表格.json》导入,待后台复核';
+
+const data = JSON.parse(fs.readFileSync(dataPath, 'utf8'));
+const offices = data['南宁写字楼信息表'] || [];
+const communities = data['南宁住宅小区信息表'] || [];
+const businessDistrictOverrides = {
+ '五象航洋城': '五象航洋城商圈'
+};
+const officialLocationEntries = [
+ {
+ region: '青秀区', businessDistrict: '青秀万达',
+ title: '滨湖路站连接青秀万达与南湖公共配套',
+ content: '南宁轨道交通3号线滨湖路站位于滨湖路与长湖路交汇处,北邻青秀万达、南近南湖公园。B口周边有南湖公园和南宁市卫生健康委员会,C口周边有南宁市第二妇幼保健院、南宁市口腔医院,D口周边有南宁市第五十四中学、埌东小学。',
+ tags: ['轨道交通', '商业配套', '公园绿地', '公共服务'],
+ sourceNote: '南宁轨道交通集团有限责任公司《滨湖路站》 http://www.nngdjt.com/html/service1c/line/0313.html'
+ },
+ {
+ region: '青秀区', businessDistrict: '东盟商务区',
+ title: '万象城站的商业、办公与公园配套',
+ content: '南宁轨道交通1号线万象城站位于东盟商务区核心区,设置在民族大道与青秀路、景秀路交叉路口地下。运营方列示,A口连接水电大厦等办公区,C口可通向万象城购物消费中心,B口周边有石门森林公园和多个居住小区。',
+ tags: ['轨道交通', '商业配套', '商务办公', '公园绿地'],
+ sourceNote: '南宁轨道交通集团有限责任公司《万象城站》 http://www.nngdjt.com/html/service1c/line/0119.html'
+ },
+ {
+ region: '青秀区', businessDistrict: '东盟商务区',
+ title: '东盟商务区站周边商业、金融与社区卫生服务',
+ content: '南宁轨道交通1号线东盟商务区站位于民族大道北侧。运营方列示,A、D口靠近凤岭儿童公园,B、C口周边有三祺广场、广西金融广场、东盟财经中心、东盟盛天地等商业和办公配套;D口周边有青秀区南湖凤岭北社区卫生服务中心。',
+ tags: ['轨道交通', '商业配套', '商务办公', '公园绿地', '公共服务'],
+ sourceNote: '南宁轨道交通集团有限责任公司《东盟商务区站》 http://www.nngdjt.com/html/service1c/line/0120.html'
+ },
+ {
+ region: '青秀区', businessDistrict: '凤岭北',
+ title: '凤岭儿童公园的公园绿地与休闲配套',
+ content: '凤岭儿童公园位于南宁市青秀区凤岭片区北侧核心区,北临月湾路、南面云景路,与凤岭冲沟相邻,占地834亩。政府页面记载,公园建设包括园内道路、基础绿化、园林构筑、广场铺装、公共设施和配套服务设施。',
+ tags: ['公园绿地', '文化休闲', '运动健身'],
+ sourceNote: '南宁市人民政府《凤岭儿童公园》 https://www.nanning.gov.cn/zjnn/nnly/t2566232.html'
+ },
+ {
+ region: '青秀区', businessDistrict: '凤岭北',
+ title: '凤岭站连接凤岭居住区、教育与公园',
+ content: '南宁轨道交通1号线凤岭站位于凤岭立交以东、民族大道北侧。运营方列示,B1口周边有南宁市第一幼儿园青林分园,B2口周边有南宁市翡翠园学校,C口通向埌东公园;埌东客运站位于车站以东约900米处。',
+ tags: ['轨道交通', '公共服务', '公园绿地'],
+ sourceNote: '南宁轨道交通集团有限责任公司《凤岭站》 http://www.nngdjt.com/html/service1c/line/0121.html'
+ },
+ {
+ region: '青秀区', businessDistrict: '埌东客运站片区',
+ title: '埌东客运站的公路客运与地铁接驳',
+ content: '埌东客运站位于民族大道东端与仙葫大道交叉处,地面上方为埌东汽车客运站。站点B、C口对应客运站一侧,E口周边为市公交总站,可作为片区公路客运、公交与地铁接驳信息使用。',
+ tags: ['轨道交通', '公共服务'],
+ sourceNote: '南宁轨道交通集团有限责任公司《埌东客运站》 http://www.nngdjt.com/html/service1c/line/0122.html'
+ },
+ {
+ region: '青秀区', businessDistrict: '会展中心',
+ title: '会展中心站与航洋国际城及会展配套',
+ content: '会展中心站位于民族大道与会展路交叉路口。B、B1口通往南宁国际会展中心,D口无缝连接航洋国际城;官方资料将该站周边规划概括为居住、商业及行政办公用地。',
+ tags: ['轨道交通', '商业配套', '商务办公', '公共服务'],
+ sourceNote: '南宁轨道交通集团有限责任公司《会展中心站》 http://www.nngdjt.com/html/service1c/line/0118.html'
+ },
+ {
+ region: '青秀区', businessDistrict: '南湖片区',
+ title: '南湖站周边绿地、政务与金融办公配套',
+ content: '南湖站位于南湖大桥东端、南湖公园绿地内。A口周边有南宁市人民政府,B口通往南湖公园,C口周边列有自治区税务局、自治区发展和改革委员会及多家银行。',
+ tags: ['轨道交通', '公园绿地', '商务办公', '公共服务'],
+ sourceNote: '南宁轨道交通集团有限责任公司《南湖站》 http://www.nngdjt.com/html/service1c/line/0116.html'
+ },
+ {
+ region: '青秀区', businessDistrict: '金湖片区',
+ title: '金湖广场站的双线换乘与民歌湖配套',
+ content: '金湖广场站位于金湖东环路与民族大道交叉口,是1号线与3号线换乘站。站点周边包括金湖广场、民歌湖、南宁书城、购物中心、金融机构、南宁市政务服务中心及南宁十四中埌东校区。',
+ tags: ['轨道交通', '商业配套', '文化休闲', '商务办公', '公共服务'],
+ sourceNote: '南宁轨道交通集团有限责任公司《金湖广场站》 http://www.nngdjt.com/html/service1c/line/0314.html'
+ },
+ {
+ region: '青秀区', businessDistrict: '凤岭南-青秀山片区',
+ title: '青秀山站与青秀山西门、青秀湖公园',
+ content: '青秀山站设置4个出入口,B口位于青秀山风景区西门及青秀山管委会一侧;A1口周边包括青秀湖公园。A3口设置无障碍电梯,周边有多条公交线路。',
+ tags: ['轨道交通', '公园绿地', '文化休闲', '公共服务'],
+ sourceNote: '南宁轨道交通集团有限责任公司《青秀山站》 http://www.nngdjt.com/html/service1c/line/0317.html'
+ },
+ {
+ region: '良庆区', businessDistrict: '市博物馆片区',
+ title: '市博物馆站与文化艺术中心配套',
+ content: '市博物馆站位于博艺路与彩凤路交叉路口,A口周边为广西文化艺术中心,D口周边为南宁市博物馆、金龙路小学及良庆区金龙幼儿园。',
+ tags: ['轨道交通', '文化休闲', '公共服务'],
+ sourceNote: '南宁轨道交通集团有限责任公司《市博物馆》 http://www.nngdjt.com/html/service1c/line/0318.html'
+ },
+ {
+ region: '江南区', businessDistrict: '江南万达',
+ title: '亭洪路沿线的商业、文化与公园节点',
+ content: '南宁市公开报道将亭洪路描述为江南区城市商贸业和文化旅游的主轴线,并列示江南万达商圈、百益·上河城、华润江南中心、江南公园、广西海吉星、火车南站和国际铁路港等节点分布在亭洪路两侧及周边。报道同时记载,亭洪路延长线建成后将江南万达商圈与沙井商圈连接起来。',
+ tags: ['商业配套', '文化休闲', '公园绿地', '商务办公'],
+ sourceNote: '南宁市人民政府《江南区发达路网赋能产城高度融合发展》 https://www.nanning.gov.cn/ywzx/xqdt/2022xqdt/t5048033.html'
+ },
+ {
+ region: '江南区', businessDistrict: '江南公园片区',
+ title: '江南公园的生态与文化休闲节点',
+ content: '南宁市江南公园位于壮锦大道17号、紧邻江南区政府,西邻壮锦大道、东接金华路、北靠亭洪路延长线及湘桂铁路线。政府页面记载,公园总面积798亩,于2018年6月正式对外开放,以休闲、生态、文化为主旨,是市级综合性城市公园。',
+ tags: ['公园绿地', '文化休闲', '公共服务'],
+ sourceNote: '南宁市文化广电和旅游局《南宁市江南公园》 https://www.nanning.gov.cn/zjnn/nnly/t4137277.html'
+ },
+ {
+ region: '江南区', businessDistrict: '亭子码头片区',
+ title: '邕江亭子码头滨水公共空间',
+ content: '南宁市政府公开报道记载,邕江亭子码头于2019年1月正式对市民开放,码头岸线长约500米;报道将其定位为面向邕江观景的滨水码头节点。',
+ tags: ['文化休闲', '公共服务'],
+ sourceNote: '南宁市人民政府《邕江亭子码头正式开放》 https://www.nanning.gov.cn/ywzx/nnyw/2019nzwdt/t1593403.html'
+ },
+ {
+ region: '西乡塘区', businessDistrict: '西乡塘大学东路',
+ title: '广西大学站的高校、社区与公共服务节点',
+ content: '南宁轨道交通1号线广西大学站位于大学东路与明秀西路交汇处下方。运营方列示,B1口周边有城市碧园、时代天骄和南宁高级技工学校,C口周边有南宁市第二小学,D口周边有西乡塘街道办事处和五里亭第一小学,F口周边有广西大学南门。',
+ tags: ['轨道交通', '公共服务'],
+ sourceNote: '南宁轨道交通集团有限责任公司《广西大学站》 http://www.nngdjt.com/html/service1c/line/0109.html'
+ },
+ {
+ region: '西乡塘区', businessDistrict: '西乡塘大学东路',
+ title: '鲁班路站的学校、医疗与公园节点',
+ content: '南宁轨道交通1号线鲁班路站位于大学东路与鲁班路交汇处下方。运营方列示,A口周边有明月湖公园、南宁市第二十中学和南宁市公安局西乡塘分局,B口周边有广西大学。',
+ tags: ['轨道交通', '公园绿地', '公共服务'],
+ sourceNote: '南宁轨道交通集团有限责任公司《鲁班路站》 http://www.nngdjt.com/html/service1c/line/0108.html'
+ },
+ {
+ region: '西乡塘区', businessDistrict: '相思湖片区',
+ title: '民族大学站的高校与基层医疗节点',
+ content: '南宁轨道交通1号线民族大学站位于大学东路与新村大道交叉路口,沿大学东路设置。运营方列示,车站西北侧有西乡塘卫生院,东北侧有广西民族大学和相思湖国际大酒店,西南侧有广西交通运输学校、广西外国语学校,东南侧有广西机电职业技术学院、广西社会主义学院和西乡塘派出所。',
+ tags: ['轨道交通', '公共服务'],
+ sourceNote: '南宁轨道交通集团有限责任公司《民族大学站》 http://www.nngdjt.com/html/service1c/line/0105.html'
+ },
+ {
+ region: '西乡塘区', businessDistrict: '南宁动物园片区',
+ title: '动物园站的亲子科普主题公共空间',
+ content: '南宁市2026年公开报道记载,南宁地铁1号线动物园站毗邻南宁动物园,并由南宁市动物园与南宁地铁围绕动物文化、科普教育和儿童友好理念打造主题车站;站内设置动物IP科普墙、动物主题儿童休息区和小象主题母婴室等场景。',
+ tags: ['轨道交通', '文化休闲', '公共服务'],
+ sourceNote: '南宁市人民政府《IP内容+空间资源 南宁地铁1号线动物园站化身童趣乐园》 https://www.nanning.gov.cn/ywzx/tpxw/t6647075.html'
+ },
+ {
+ region: '兴宁区', businessDistrict: '兴宁老城区',
+ title: '三街两巷历史文化街区',
+ content: '南宁市三街两巷历史街区是南宁主城区规模最大的历史文化街区,由解放路、兴宁路、民生路三条老街和金狮巷、银狮巷两条古巷组成,汇聚文物保护单位及历史建筑,是近代南宁较集中的骑楼建筑片区。',
+ tags: ['文化休闲', '商业配套'],
+ sourceNote: '南宁市人民政府《历史文化街区“三街两巷”露芳容》 https://www.nanning.gov.cn/zjnn/mswh/t1526950.html'
+ },
+ {
+ region: '邕宁区', businessDistrict: '龙岗',
+ title: '轨道交通4号线后通段接入龙岗',
+ content: '南宁市公开报道记载,2025年9月轨道交通4号线一期工程后通段(楞塘村站至龙岗站)开通运营,龙岗站成为该后通段终点站,邕宁区由此接入轨道交通网络。',
+ tags: ['轨道交通'],
+ sourceNote: '邕宁区人民政府《喜大普奔!地铁4号线后通段开通运营,邕宁迈入“地铁时代”!》 https://www.nanning.gov.cn/ywzx/xqdt/2025nxqdt/t6428425.html'
+ },
+ {
+ region: '邕宁区', businessDistrict: '园博园片区',
+ title: '南宁园博园的园林与文化展示节点',
+ content: '南宁园博园是第十二届中国国际园林博览会举办地,位于邕宁区八尺江畔顶蛳山地块,占地276公顷,2018年12月建成开放。政府页面记载,园内有新石器时代文化遗址、丘陵与原生山林、湖塘和湿地,并设中华城市展园、东盟园、丝路园、广西园、设计师园和企业园等主题展园区。',
+ tags: ['公园绿地', '文化休闲'],
+ sourceNote: '南宁市人民政府《南宁园博园》 https://www.nanning.gov.cn/zjnn/nnly/t2566279.html'
+ },
+ {
+ region: '青秀区', businessDistrict: '埌西片区',
+ title: '埌西站周边教育、人社与生活服务',
+ content: '埌西站位于金湖路与桂春路交叉路口。站点周边列有南宁市教育局、南宁市人力资源和社会保障局、南宁市民政局、广西疾病预防控制中心、埌西综合市场、人才市场及多所学校。',
+ tags: ['轨道交通', '公共服务', '商业配套'],
+ sourceNote: '南宁轨道交通集团有限责任公司《埌西站》 http://www.nngdjt.com/html/service1c/line/0315.html'
+ }
+];
+
+if (sourceNote.length > 500 || officialLocationEntries.some((item) => item.sourceNote.length > 500)) {
+ throw new Error('知识条目的来源说明超过数据库字段长度');
+}
+
+function sql(value) {
+ if (value === null || value === undefined) {
+ return 'NULL';
+ }
+ return `'${String(value).replace(/'/g, "''")}'`;
+}
+
+function known(value) {
+ return value !== undefined && value !== null && String(value).trim() !== ''
+ && String(value).trim() !== '待定';
+}
+
+function exactNumber(value) {
+ const text = String(value || '').trim();
+ return /^\d+(?:\.\d+)?$/.test(text) ? text : null;
+}
+
+function businessDistrictName(item) {
+ return businessDistrictOverrides[item['楼盘名称']] || item['所属商圈'];
+}
+
+function text(parts) {
+ return parts.filter(([_, value]) => known(value))
+ .map(([label, value]) => `${label}:${value}`)
+ .join(';');
+}
+
+function locationFilter(item) {
+ return [
+ 'FROM house_knowledge_location community',
+ 'JOIN house_knowledge_location business_district',
+ ' ON business_district.location_id = community.parent_location_id',
+ 'WHERE community.tenant_id = @house_knowledge_tenant_id',
+ " AND community.city = '南宁'",
+ " AND community.location_type = 'community'",
+ ` AND community.location_name = ${sql(item['楼盘名称'])}`,
+ ' AND community.deleted = 0',
+ ' AND business_district.tenant_id = @house_knowledge_tenant_id',
+ " AND business_district.location_type = 'business_district'",
+ ` AND business_district.location_name = ${sql(businessDistrictName(item))}`,
+ ' AND business_district.deleted = 0'
+ ].join('\n');
+}
+
+function tagInsert(topic, item, tags) {
+ const values = [...tags].filter(Boolean).map(sql).join(', ');
+ return [
+ 'INSERT IGNORE INTO house_knowledge_entry_tag (entry_id, tag_id, tenant_id)',
+ 'SELECT entry.entry_id, tag.tag_id, @house_knowledge_tenant_id',
+ 'FROM house_knowledge_entry entry',
+ 'JOIN house_knowledge_location community ON community.location_id = entry.location_id',
+ 'JOIN house_knowledge_location business_district',
+ ' ON business_district.location_id = community.parent_location_id',
+ 'JOIN house_knowledge_tag tag ON tag.tenant_id = @house_knowledge_tenant_id',
+ ' AND tag.deleted = 0',
+ 'WHERE entry.tenant_id = @house_knowledge_tenant_id',
+ ' AND entry.deleted = 0',
+ ' AND entry.status = 0',
+ ` AND entry.topic = ${sql(topic)}`,
+ ` AND entry.source_note = ${sql(sourceNote)}`,
+ " AND community.city = '南宁'",
+ " AND community.location_type = 'community'",
+ ` AND community.location_name = ${sql(item['楼盘名称'])}`,
+ ` AND business_district.location_name = ${sql(businessDistrictName(item))}`,
+ ` AND tag.tag_name IN (${values});`
+ ].join('\n');
+}
+
+function entryInsert(item, topic, title, content, fields) {
+ return [
+ 'INSERT IGNORE INTO house_knowledge_entry (',
+ ' location_id, topic, title, content, property_company, property_fees,',
+ ' water_billing_type, water_unit_price, electricity_billing_type, electricity_unit_price,',
+ ' parking_available, parking_fee, verified_date, source_note, status, tenant_id, deleted',
+ ')',
+ 'SELECT',
+ ' community.location_id,',
+ ` ${sql(topic)},`,
+ ` ${sql(title)},`,
+ ` ${sql(content || null)},`,
+ ` ${sql(fields.propertyCompany)},`,
+ ` ${fields.propertyFees || 'NULL'},`,
+ ` ${sql(fields.waterBillingType)},`,
+ ` ${fields.waterUnitPrice || 'NULL'},`,
+ ` ${sql(fields.electricityBillingType)},`,
+ ` ${fields.electricityUnitPrice || 'NULL'},`,
+ ` ${fields.parkingAvailable === null ? 'NULL' : fields.parkingAvailable ? '1' : '0'},`,
+ ` ${sql(fields.parkingFee)},`,
+ ' CURDATE(),',
+ ` ${sql(sourceNote)},`,
+ ' 0, @house_knowledge_tenant_id, 0',
+ locationFilter(item) + ';'
+ ].join('\n');
+}
+
+function parkingAvailable(item) {
+ if (item['地下停车场'] === '有') {
+ return true;
+ }
+ if (item['地下停车场'] === '无') {
+ return false;
+ }
+ return known(item['停车位数量']) ? true : null;
+}
+
+function entryBlocks(item, type) {
+ const name = item['楼盘名称'];
+ const typeTag = type === 'office' ? '写字楼' : '住宅小区';
+ const propertyContent = text([
+ ['物业类型', item['物业类型']],
+ ['物业公司', item['物业公司']],
+ ['物业费标准', item['物业费标准(元/㎡/月)']],
+ ['物业服务时间', item['物业服务时间']],
+ ['内部配套', item['内部配套']],
+ ['总层数', item['总层数']],
+ ['建成年份', item['建成年份']],
+ ['产权年限', item['产权年限']],
+ ['备注', item['备注']]
+ ]);
+ const utilitiesContent = text([
+ ['水电类型', item['水电类型']],
+ ['水费标准', item['水费标准(元/吨)']],
+ ['电费标准', item['电费标准(元/度)']],
+ ['24小时供水供电', item['24小时供水供电']]
+ ]);
+ const parkingContent = text([
+ ['停车位数量', item['停车位数量']],
+ ['车位配比', item['车位配比']],
+ ['地下停车场', item['地下停车场']],
+ ['充电桩配置', item['充电桩配置']],
+ ['临停收费标准', item['临停收费标准']],
+ ['月保收费标准', item['月保收费标准']]
+ ]);
+ const otherContent = text([
+ ['所属区域', item['所属区域']],
+ ['详细地址', item['详细地址']],
+ ['所属商圈', businessDistrictName(item)],
+ ['周边配套', item['周边配套']]
+ ]);
+ const propertyTags = new Set(['物业服务', typeTag]);
+ const utilitiesTags = new Set(['水电计费', typeTag]);
+ const parkingTags = new Set(['停车信息', typeTag]);
+ const otherTags = new Set(['商业配套', typeTag]);
+ if (item['物业服务时间'] === '24小时') {
+ propertyTags.add('24小时服务');
+ }
+ if (item['充电桩配置'] === '有') {
+ parkingTags.add('充电设施');
+ }
+ if (String(item['周边配套'] || '').includes('地铁')) {
+ otherTags.add('轨道交通');
+ otherTags.add('地铁出行');
+ }
+ if (type === 'office') {
+ otherTags.add('商务办公');
+ }
+
+ return [
+ entryInsert(item, 'property', `${name}的物业服务`, propertyContent, {
+ propertyCompany: known(item['物业公司']) ? item['物业公司'] : null,
+ propertyFees: exactNumber(item['物业费标准(元/㎡/月)']),
+ waterBillingType: null,
+ waterUnitPrice: null,
+ electricityBillingType: null,
+ electricityUnitPrice: null,
+ parkingAvailable: null,
+ parkingFee: null
+ }),
+ tagInsert('property', item, propertyTags),
+ entryInsert(item, 'utilities', `${name}的水电计费`, utilitiesContent, {
+ propertyCompany: null,
+ propertyFees: null,
+ waterBillingType: known(item['水电类型']) ? item['水电类型'] : null,
+ waterUnitPrice: exactNumber(item['水费标准(元/吨)']),
+ electricityBillingType: known(item['水电类型']) ? item['水电类型'] : null,
+ electricityUnitPrice: exactNumber(item['电费标准(元/度)']),
+ parkingAvailable: null,
+ parkingFee: null
+ }),
+ tagInsert('utilities', item, utilitiesTags),
+ entryInsert(item, 'parking', `${name}的停车与充电`, parkingContent, {
+ propertyCompany: null,
+ propertyFees: null,
+ waterBillingType: null,
+ waterUnitPrice: null,
+ electricityBillingType: null,
+ electricityUnitPrice: null,
+ parkingAvailable: parkingAvailable(item),
+ parkingFee: text([
+ ['临停', item['临停收费标准']],
+ ['月保', item['月保收费标准']]
+ ]) || null
+ }),
+ tagInsert('parking', item, parkingTags),
+ entryInsert(item, 'other', `${name}的地段与周边配套`, otherContent, {
+ propertyCompany: null,
+ propertyFees: null,
+ waterBillingType: null,
+ waterUnitPrice: null,
+ electricityBillingType: null,
+ electricityUnitPrice: null,
+ parkingAvailable: null,
+ parkingFee: null
+ }),
+ tagInsert('other', item, otherTags)
+ ];
+}
+
+function officialLocationInsert(item) {
+ return [
+ 'INSERT IGNORE INTO house_knowledge_location (',
+ ' city, location_type, location_name, parent_location_id, status, tenant_id, deleted',
+ ')',
+ 'SELECT',
+ ` '南宁', 'business_district', ${sql(item.businessDistrict)}, region.location_id,`,
+ ' 0, @house_knowledge_tenant_id, 0',
+ 'FROM house_knowledge_location region',
+ 'WHERE region.tenant_id = @house_knowledge_tenant_id',
+ " AND region.city = '南宁'",
+ " AND region.location_type = 'region'",
+ ` AND region.location_name = ${sql(item.region)}`,
+ ' AND region.deleted = 0',
+ ' AND NOT EXISTS (',
+ ' SELECT 1 FROM house_knowledge_location existing',
+ ' WHERE existing.tenant_id = @house_knowledge_tenant_id',
+ " AND existing.city = '南宁'",
+ " AND existing.location_type = 'business_district'",
+ ` AND existing.location_name = ${sql(item.businessDistrict)}`,
+ ' AND existing.deleted = 0',
+ ' );'
+ ].join('\n');
+}
+
+function officialEntryInsert(item) {
+ return [
+ 'INSERT IGNORE INTO house_knowledge_entry (',
+ ' location_id, topic, title, content, verified_date, source_note,',
+ ' status, tenant_id, deleted',
+ ')',
+ 'SELECT',
+ ' business_district.location_id,',
+ " 'other',",
+ ` ${sql(item.title)},`,
+ ` ${sql(item.content)},`,
+ " '2026-08-02',",
+ ` ${sql(item.sourceNote)},`,
+ ' 0, @house_knowledge_tenant_id, 0',
+ 'FROM house_knowledge_location business_district',
+ 'WHERE business_district.tenant_id = @house_knowledge_tenant_id',
+ " AND business_district.city = '南宁'",
+ " AND business_district.location_type = 'business_district'",
+ ` AND business_district.location_name = ${sql(item.businessDistrict)}`,
+ ' AND business_district.deleted = 0;'
+ ].join('\n');
+}
+
+function officialTagInsert(item) {
+ return [
+ 'INSERT IGNORE INTO house_knowledge_entry_tag (entry_id, tag_id, tenant_id)',
+ 'SELECT entry.entry_id, tag.tag_id, @house_knowledge_tenant_id',
+ 'FROM house_knowledge_entry entry',
+ 'JOIN house_knowledge_location business_district ON business_district.location_id = entry.location_id',
+ 'JOIN house_knowledge_tag tag ON tag.tenant_id = @house_knowledge_tenant_id',
+ ' AND tag.deleted = 0',
+ 'WHERE entry.tenant_id = @house_knowledge_tenant_id',
+ ' AND entry.deleted = 0',
+ ' AND entry.status = 0',
+ " AND entry.topic = 'other'",
+ ` AND entry.source_note = ${sql(item.sourceNote)}`,
+ " AND business_district.city = '南宁'",
+ " AND business_district.location_type = 'business_district'",
+ ` AND business_district.location_name = ${sql(item.businessDistrict)}`,
+ ` AND tag.tag_name IN (${item.tags.map(sql).join(', ')});`
+ ].join('\n');
+}
+
+const records = [
+ ...offices.map((item) => ({ item, type: 'office' })),
+ ...communities.map((item) => ({ item, type: 'community' }))
+];
+const regions = [...new Set(records.map(({ item }) => item['所属区域']).filter(known))];
+const districts = [...new Map(records
+ .filter(({ item }) => known(item['所属区域']) && known(businessDistrictName(item)))
+ .map(({ item }) => [`${item['所属区域']}\u0000${businessDistrictName(item)}`, item])).values()];
+
+const lines = [
+ '-- 以下数据由南宁房产知识库结构化表格.json 和官方公开资料生成。',
+ `-- 共 ${offices.length} 个写字楼、${communities.length} 个住宅小区和 ${officialLocationEntries.length} 条官方片区知识;来源说明仅后台可见。`,
+ ''
+];
+
+for (const region of regions) {
+ lines.push([
+ 'INSERT IGNORE INTO house_knowledge_location (',
+ ' city, location_type, location_name, parent_location_id, status, tenant_id, deleted',
+ ') VALUES (',
+ ` '南宁', 'region', ${sql(region)}, 0, 0, @house_knowledge_tenant_id, 0`,
+ ');',
+ ''
+ ].join('\n'));
+}
+
+for (const item of districts) {
+ lines.push([
+ 'INSERT IGNORE INTO house_knowledge_location (',
+ ' city, location_type, location_name, parent_location_id, status, tenant_id, deleted',
+ ')',
+ 'SELECT',
+ ` '南宁', 'business_district', ${sql(businessDistrictName(item))}, region.location_id,`,
+ ' 0, @house_knowledge_tenant_id, 0',
+ 'FROM house_knowledge_location region',
+ 'WHERE region.tenant_id = @house_knowledge_tenant_id',
+ " AND region.city = '南宁'",
+ " AND region.location_type = 'region'",
+ ` AND region.location_name = ${sql(item['所属区域'])}`,
+ ' AND region.deleted = 0',
+ ' AND NOT EXISTS (',
+ ' SELECT 1 FROM house_knowledge_location existing',
+ ' WHERE existing.tenant_id = @house_knowledge_tenant_id',
+ " AND existing.city = '南宁'",
+ " AND existing.location_type = 'business_district'",
+ ` AND existing.location_name = ${sql(businessDistrictName(item))}`,
+ ' AND existing.deleted = 0',
+ ' );',
+ ''
+ ].join('\n'));
+}
+
+for (const item of officialLocationEntries) {
+ lines.push(officialLocationInsert(item), '');
+}
+
+for (const { item } of records) {
+ lines.push([
+ 'INSERT IGNORE INTO house_knowledge_location (',
+ ' city, location_type, location_name, parent_location_id, status, tenant_id, deleted',
+ ')',
+ 'SELECT',
+ ` '南宁', 'community', ${sql(item['楼盘名称'])}, business_district.location_id,`,
+ ' 0, @house_knowledge_tenant_id, 0',
+ 'FROM house_knowledge_location business_district',
+ 'WHERE business_district.tenant_id = @house_knowledge_tenant_id',
+ " AND business_district.city = '南宁'",
+ " AND business_district.location_type = 'business_district'",
+ ` AND business_district.location_name = ${sql(businessDistrictName(item))}`,
+ ' AND business_district.deleted = 0;',
+ ''
+ ].join('\n'));
+}
+
+for (const { item, type } of records) {
+ lines.push(...entryBlocks(item, type), '');
+}
+
+for (const item of officialLocationEntries) {
+ lines.push(officialEntryInsert(item), officialTagInsert(item), '');
+}
+
+const seed = fs.readFileSync(seedPath, 'utf8');
+const start = seed.indexOf(startMarker);
+const end = seed.indexOf(endMarker);
+if (start < 0 || end < 0 || end < start) {
+ throw new Error('未找到南宁结构化数据生成区段标记');
+}
+
+const generated = `${startMarker}\n${lines.join('\n')}${endMarker}`;
+fs.writeFileSync(seedPath, `${seed.slice(0, start)}${generated}${seed.slice(end + endMarker.length)}`, 'utf8');
+console.log(`已生成 ${records.length} 个地点的房源知识种子数据。`);
diff --git a/src/main/java/com/gxwebsoft/house/ai/AmapMcpClient.java b/src/main/java/com/gxwebsoft/house/ai/AmapMcpClient.java
new file mode 100644
index 0000000..70bfc9e
--- /dev/null
+++ b/src/main/java/com/gxwebsoft/house/ai/AmapMcpClient.java
@@ -0,0 +1,243 @@
+package com.gxwebsoft.house.ai;
+
+import cn.hutool.core.util.StrUtil;
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONArray;
+import com.alibaba.fastjson.JSONObject;
+import org.springframework.stereotype.Component;
+
+import javax.annotation.Resource;
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.nio.charset.StandardCharsets;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicLong;
+
+/**
+ * 高德 Streamable HTTP MCP 的最小客户端。
+ *
+ *
只实现找房智能体所需的初始化、工具发现和只读工具调用,不将 MCP
+ * 会话或高德 Key 暴露给客户端。
+ */
+@Component
+public class AmapMcpClient {
+
+ private static final String JSON_RPC_VERSION = "2.0";
+ private static final String MCP_PROTOCOL_VERSION = "2025-03-26";
+
+ @Resource
+ private HouseAmapMcpProperties properties;
+
+ private final Object sessionLock = new Object();
+ private final AtomicLong requestId = new AtomicLong(1);
+ private volatile boolean initialized;
+ private volatile String sessionId;
+ private volatile String protocolVersion = MCP_PROTOCOL_VERSION;
+ private volatile List toolCache = Collections.emptyList();
+ private volatile long toolCacheExpiresAt;
+
+ public boolean isEnabled() {
+ return properties.isEnabled() && StrUtil.isNotBlank(properties.getUrl());
+ }
+
+ /**
+ * 获取 MCP 服务当前公开的工具。配置未启用时不发起网络请求。
+ */
+ public List listTools() {
+ if (!isEnabled()) {
+ return Collections.emptyList();
+ }
+ long now = System.currentTimeMillis();
+ List cached = toolCache;
+ if (!cached.isEmpty() && now < toolCacheExpiresAt) {
+ return cached;
+ }
+ synchronized (sessionLock) {
+ now = System.currentTimeMillis();
+ if (!toolCache.isEmpty() && now < toolCacheExpiresAt) {
+ return toolCache;
+ }
+ ensureInitialized();
+ JSONObject result = request("tools/list", new JSONObject());
+ JSONArray tools = result.getJSONArray("tools");
+ if (tools == null) {
+ throw new IllegalStateException("高德 MCP 未返回工具列表");
+ }
+ List loaded = tools.toJavaList(JSONObject.class);
+ toolCache = Collections.unmodifiableList(loaded);
+ toolCacheExpiresAt = now + Math.max(0, properties.getToolCacheTtlMs());
+ return toolCache;
+ }
+ }
+
+ /**
+ * 调用已由 MCP 服务公开的工具。返回完整的 MCP ToolResult,供模型基于事实回答。
+ */
+ public JSONObject callTool(String toolName, JSONObject arguments) {
+ if (!isEnabled()) {
+ throw new IllegalStateException("高德 MCP 未启用或未配置服务地址");
+ }
+ if (StrUtil.isBlank(toolName)) {
+ throw new IllegalArgumentException("高德 MCP 工具名称不能为空");
+ }
+ synchronized (sessionLock) {
+ ensureInitialized();
+ JSONObject params = new JSONObject();
+ params.put("name", toolName);
+ params.put("arguments", arguments == null ? new JSONObject() : arguments);
+ JSONObject result = request("tools/call", params);
+ if (Boolean.TRUE.equals(result.getBoolean("isError"))) {
+ throw new IllegalStateException("高德 MCP 工具调用失败:" + extractToolError(result));
+ }
+ return result;
+ }
+ }
+
+ private void ensureInitialized() {
+ if (initialized) {
+ return;
+ }
+ JSONObject params = new JSONObject();
+ params.put("protocolVersion", MCP_PROTOCOL_VERSION);
+ params.put("capabilities", new JSONObject());
+ JSONObject clientInfo = new JSONObject();
+ clientInfo.put("name", "aishangjia-house-ai");
+ clientInfo.put("version", "1.0.0");
+ params.put("clientInfo", clientInfo);
+
+ JSONObject result = request("initialize", params);
+ String negotiatedVersion = result.getString("protocolVersion");
+ if (StrUtil.isNotBlank(negotiatedVersion)) {
+ protocolVersion = negotiatedVersion;
+ }
+ notifyInitialized();
+ initialized = true;
+ }
+
+ private void notifyInitialized() {
+ JSONObject notification = new JSONObject();
+ notification.put("jsonrpc", JSON_RPC_VERSION);
+ notification.put("method", "notifications/initialized");
+ post(notification, true);
+ }
+
+ private JSONObject request(String method, JSONObject params) {
+ JSONObject payload = new JSONObject();
+ payload.put("jsonrpc", JSON_RPC_VERSION);
+ payload.put("id", requestId.getAndIncrement());
+ payload.put("method", method);
+ payload.put("params", params == null ? new JSONObject() : params);
+ JSONObject response = post(payload, false);
+ if (response == null) {
+ throw new IllegalStateException("高德 MCP 未返回响应");
+ }
+ JSONObject error = response.getJSONObject("error");
+ if (error != null) {
+ throw new IllegalStateException("高德 MCP 调用失败:" + error.getString("message"));
+ }
+ JSONObject result = response.getJSONObject("result");
+ if (result == null) {
+ String providerMessage = firstNotBlank(response.getString("info"), response.getString("message"));
+ throw new IllegalStateException(StrUtil.isBlank(providerMessage)
+ ? "高德 MCP 返回了无效响应" : "高德 MCP 调用失败:" + providerMessage);
+ }
+ return result;
+ }
+
+ private JSONObject post(JSONObject payload, boolean notification) {
+ HttpURLConnection connection = null;
+ try {
+ connection = (HttpURLConnection) new URL(properties.getUrl()).openConnection();
+ connection.setRequestMethod("POST");
+ connection.setDoOutput(true);
+ connection.setConnectTimeout(properties.getTimeoutMs());
+ connection.setReadTimeout(properties.getTimeoutMs());
+ connection.setRequestProperty("Content-Type", "application/json; charset=UTF-8");
+ connection.setRequestProperty("Accept", "application/json, text/event-stream");
+ connection.setRequestProperty("MCP-Protocol-Version", protocolVersion);
+ if (StrUtil.isNotBlank(sessionId)) {
+ connection.setRequestProperty("Mcp-Session-Id", sessionId);
+ }
+ try (OutputStream output = connection.getOutputStream()) {
+ output.write(payload.toJSONString().getBytes(StandardCharsets.UTF_8));
+ }
+
+ int status = connection.getResponseCode();
+ String responseBody = readBody(status >= 400 ? connection.getErrorStream() : connection.getInputStream());
+ String returnedSessionId = connection.getHeaderField("Mcp-Session-Id");
+ if (StrUtil.isNotBlank(returnedSessionId)) {
+ sessionId = returnedSessionId;
+ }
+ if (status < 200 || status >= 300) {
+ throw new IllegalStateException("高德 MCP HTTP 请求失败,状态码:" + status);
+ }
+ if (notification || StrUtil.isBlank(responseBody)) {
+ return null;
+ }
+ return parseResponse(responseBody);
+ } catch (IOException e) {
+ throw new IllegalStateException("连接高德 MCP 失败", e);
+ } finally {
+ if (connection != null) {
+ connection.disconnect();
+ }
+ }
+ }
+
+ private JSONObject parseResponse(String body) {
+ String trimmed = body == null ? "" : body.trim();
+ String[] lines = trimmed.split("\\r?\\n");
+ for (int index = lines.length - 1; index >= 0; index--) {
+ String line = lines[index].trim();
+ if (line.startsWith("data:")) {
+ trimmed = line.substring("data:".length()).trim();
+ break;
+ }
+ }
+ try {
+ return JSON.parseObject(trimmed);
+ } catch (Exception e) {
+ throw new IllegalStateException("高德 MCP 返回的不是有效 JSON", e);
+ }
+ }
+
+ private String readBody(InputStream stream) throws IOException {
+ if (stream == null) {
+ return null;
+ }
+ StringBuilder body = new StringBuilder();
+ try (BufferedReader reader = new BufferedReader(new InputStreamReader(stream, StandardCharsets.UTF_8))) {
+ String line;
+ while ((line = reader.readLine()) != null) {
+ body.append(line).append('\n');
+ }
+ }
+ return body.toString();
+ }
+
+ private String firstNotBlank(String first, String second) {
+ return StrUtil.isNotBlank(first) ? first : second;
+ }
+
+ private String extractToolError(JSONObject result) {
+ JSONArray content = result.getJSONArray("content");
+ if (content != null) {
+ for (Object item : content) {
+ if (!(item instanceof JSONObject)) {
+ continue;
+ }
+ String text = ((JSONObject) item).getString("text");
+ if (StrUtil.isNotBlank(text)) {
+ return text;
+ }
+ }
+ }
+ return "服务返回错误";
+ }
+}
diff --git a/src/main/java/com/gxwebsoft/house/ai/AmapMcpToolService.java b/src/main/java/com/gxwebsoft/house/ai/AmapMcpToolService.java
new file mode 100644
index 0000000..e5fe0ef
--- /dev/null
+++ b/src/main/java/com/gxwebsoft/house/ai/AmapMcpToolService.java
@@ -0,0 +1,99 @@
+package com.gxwebsoft.house.ai;
+
+import cn.hutool.core.util.StrUtil;
+import com.alibaba.fastjson.JSONArray;
+import com.alibaba.fastjson.JSONObject;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Service;
+
+import javax.annotation.Resource;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 将高德 MCP 的动态工具列表适配为模型兼容的 function calling 工具。
+ */
+@Service
+public class AmapMcpToolService {
+
+ private static final String TOOL_PREFIX = "amap_";
+ private static final Logger log = LoggerFactory.getLogger(AmapMcpToolService.class);
+
+ @Resource
+ private AmapMcpClient amapMcpClient;
+
+ /** 模型调用和工具执行发生在同一请求线程,使用线程隔离避免并发请求互相覆盖映射。 */
+ private final ThreadLocal