AI 生成: 基于AI的酒店推荐系统,集成地图路线与距离显示,使用Flask后端与Leaflet前端。

This commit is contained in:
admin
2026-10-01 15:18:18 +08:00
commit e3579ee139
6 changed files with 254 additions and 0 deletions
+31
View File
@@ -0,0 +1,31 @@
# AI Hotel Recommender
基于AI的酒店推荐系统,支持地图查看路线与距离。
## 功能
- 输入目的地、入住日期、预算等条件
- AI推荐酒店列表
- 地图显示酒店位置、路线与距离
## 技术栈
- 后端: Python Flask
- 前端: HTML/CSS/JS, Leaflet, OpenStreetMap
- AI: 简单规则引擎(可替换为真实API)
## 运行
1. 安装依赖: `pip install flask requests`
2. 启动: `python app.py`
3. 访问: http://localhost:5000
## 目录结构
```
ai-hotel-recommender/
├── app.py
├── requirements.txt
├── README.md
├── static/
│ ├── css/style.css
│ └── js/main.js
└── templates/
└── index.html
```
+69
View File
@@ -0,0 +1,69 @@
from flask import Flask, render_template, request, jsonify
import random
import math
app = Flask(__name__)
# 模拟酒店数据(实际可接入真实API)
HOTELS = [
{"id": 1, "name": "海景大酒店", "lat": 31.2304, "lng": 121.4737, "price": 800, "rating": 4.8, "tags": ["海景", "免费早餐", "健身房"]},
{"id": 2, "name": "城市商务酒店", "lat": 31.2350, "lng": 121.4800, "price": 500, "rating": 4.5, "tags": ["商务中心", "会议室", "24小时前台"]},
{"id": 3, "name": "温馨民宿", "lat": 31.2250, "lng": 121.4650, "price": 300, "rating": 4.7, "tags": ["家庭房", "厨房", "免费停车"]},
{"id": 4, "name": "豪华度假村", "lat": 31.2400, "lng": 121.4900, "price": 1500, "rating": 4.9, "tags": ["泳池", "SPA", "私人海滩"]},
{"id": 5, "name": "经济快捷酒店", "lat": 31.2200, "lng": 121.4600, "price": 200, "rating": 4.2, "tags": ["免费WiFi", "空调", "电视"]},
]
def haversine(lat1, lon1, lat2, lon2):
"""计算两点间距离(公里)"""
R = 6371
dlat = math.radians(lat2 - lat1)
dlon = math.radians(lon2 - lon1)
a = math.sin(dlat/2)**2 + math.cos(math.radians(lat1)) * math.cos(math.radians(lat2)) * math.sin(dlon/2)**2
c = 2 * math.asin(math.sqrt(a))
return R * c
def ai_recommend(destination, budget, preferences, user_lat, user_lng):
"""简单AI推荐:根据预算、偏好、距离评分"""
scored = []
for h in HOTELS:
score = 0
# 预算匹配
if budget and h["price"] <= budget:
score += 30
elif budget:
score -= 10
# 偏好匹配
for pref in preferences:
if pref in h["tags"]:
score += 20
# 评分
score += h["rating"] * 5
# 距离(越近越高)
dist = haversine(user_lat, user_lng, h["lat"], h["lng"])
score += max(0, 30 - dist * 2)
h_copy = h.copy()
h_copy["distance"] = round(dist, 2)
h_copy["score"] = round(score, 1)
scored.append(h_copy)
scored.sort(key=lambda x: x["score"], reverse=True)
return scored
@app.route('/')
def index():
return render_template('index.html')
@app.route('/api/recommend', methods=['POST'])
def recommend():
data = request.json
destination = data.get('destination', '上海')
budget = data.get('budget')
preferences = data.get('preferences', [])
user_lat = data.get('lat', 31.2304)
user_lng = data.get('lng', 121.4737)
if budget:
budget = float(budget)
results = ai_recommend(destination, budget, preferences, user_lat, user_lng)
return jsonify({"hotels": results})
if __name__ == '__main__':
app.run(debug=True)
+2
View File
@@ -0,0 +1,2 @@
Flask==2.3.3
requests==2.31.0
+26
View File
@@ -0,0 +1,26 @@
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: 'Segoe UI', Arial, sans-serif; background: #f0f2f5; color: #333; }
.container { display: flex; flex-direction: column; height: 100vh; }
header { background: linear-gradient(135deg, #667eea, #764ba2); color: white; padding: 16px 24px; }
header h1 { font-size: 22px; }
header p { font-size: 13px; opacity: 0.9; margin-top: 4px; }
.main { display: flex; flex: 1; overflow: hidden; }
.sidebar { width: 340px; background: white; padding: 20px; overflow-y: auto; box-shadow: 2px 0 8px rgba(0,0,0,0.05); }
.form-group { margin-bottom: 16px; }
.form-group label { display: block; font-size: 13px; font-weight: 600; margin-bottom: 6px; color: #555; }
.form-group input[type="text"], .form-group input[type="number"] { width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 8px; font-size: 14px; }
.checkboxes { display: flex; flex-wrap: wrap; gap: 8px; }
.checkboxes label { font-size: 13px; display: flex; align-items: center; gap: 4px; background: #f5f5f5; padding: 6px 10px; border-radius: 20px; cursor: pointer; }
button#searchBtn { width: 100%; padding: 12px; background: linear-gradient(135deg, #667eea, #764ba2); color: white; border: none; border-radius: 8px; font-size: 15px; font-weight: 600; cursor: pointer; transition: opacity 0.2s; }
button#searchBtn:hover { opacity: 0.9; }
.hotel-list { margin-top: 20px; }
.hotel-card { background: #fafafa; border: 1px solid #eee; border-radius: 10px; padding: 12px; margin-bottom: 10px; cursor: pointer; transition: all 0.2s; }
.hotel-card:hover { border-color: #667eea; box-shadow: 0 2px 8px rgba(102,126,234,0.15); }
.hotel-card.active { border-color: #667eea; background: #f0f4ff; }
.hotel-card h3 { font-size: 15px; margin-bottom: 4px; }
.hotel-card .meta { font-size: 12px; color: #888; display: flex; justify-content: space-between; }
.hotel-card .tags { margin-top: 6px; display: flex; flex-wrap: wrap; gap: 4px; }
.hotel-card .tags span { font-size: 11px; background: #e8ecff; color: #5566cc; padding: 2px 8px; border-radius: 10px; }
.placeholder { color: #aaa; font-size: 13px; text-align: center; padding: 20px 0; }
.map-container { flex: 1; }
#map { width: 100%; height: 100%; }
+76
View File
@@ -0,0 +1,76 @@
// 初始化地图
const map = L.map('map').setView([31.2304, 121.4737], 13);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '&copy; OpenStreetMap contributors'
}).addTo(map);
let markers = [];
let routeLine = null;
let userMarker = null;
// 获取用户位置(模拟为上海中心)
const userLat = 31.2304, userLng = 121.4737;
userMarker = L.marker([userLat, userLng], { icon: L.divIcon({ className: 'user-marker', html: '📍', iconSize: [24, 24] }) }).addTo(map).bindPopup('您的位置');
// 搜索按钮
document.getElementById('searchBtn').addEventListener('click', async () => {
const destination = document.getElementById('destination').value;
const budget = document.getElementById('budget').value;
const preferences = Array.from(document.querySelectorAll('.checkboxes input:checked')).map(cb => cb.value);
const res = await fetch('/api/recommend', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ destination, budget, preferences, lat: userLat, lng: userLng })
});
const data = await res.json();
renderHotels(data.hotels);
renderMarkers(data.hotels);
});
function renderHotels(hotels) {
const list = document.getElementById('hotelList');
list.innerHTML = '';
hotels.forEach((h, i) => {
const card = document.createElement('div');
card.className = 'hotel-card';
card.innerHTML = `
<h3>${h.name}</h3>
<div class="meta"><span>⭐ ${h.rating}</span><span>¥${h.price}/晚</span></div>
<div class="meta"><span>📏 ${h.distance} km</span><span>🎯 ${h.score}分</span></div>
<div class="tags">${h.tags.map(t => `<span>${t}</span>`).join('')}</div>
`;
card.addEventListener('click', () => {
document.querySelectorAll('.hotel-card').forEach(c => c.classList.remove('active'));
card.classList.add('active');
showRoute(h);
});
list.appendChild(card);
});
}
function renderMarkers(hotels) {
markers.forEach(m => map.removeLayer(m));
markers = [];
if (routeLine) { map.removeLayer(routeLine); routeLine = null; }
const bounds = [[userLat, userLng]];
hotels.forEach(h => {
const marker = L.marker([h.lat, h.lng]).addTo(map)
.bindPopup(`<b>${h.name}</b><br>¥${h.price}/晚 · ⭐${h.rating}<br>距离: ${h.distance} km`);
markers.push(marker);
bounds.push([h.lat, h.lng]);
});
map.fitBounds(bounds, { padding: [50, 50] });
}
function showRoute(hotel) {
if (routeLine) map.removeLayer(routeLine);
// 直线路线(实际可接入路由API)
const points = [[userLat, userLng], [hotel.lat, hotel.lng]];
routeLine = L.polyline(points, { color: '#667eea', weight: 4, dashArray: '8,8' }).addTo(map);
map.fitBounds(routeLine.getBounds(), { padding: [50, 50] });
L.popup()
.setLatLng([(userLat + hotel.lat) / 2, (userLng + hotel.lng) / 2])
.setContent(`距离: ${hotel.distance} km`)
.openOn(map);
}
+50
View File
@@ -0,0 +1,50 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AI 酒店推荐</title>
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
<link rel="stylesheet" href="static/css/style.css" />
</head>
<body>
<div class="container">
<header>
<h1>🏨 AI 酒店推荐</h1>
<p>智能推荐 · 地图看路线 · 距离一目了然</p>
</header>
<div class="main">
<aside class="sidebar">
<div class="form-group">
<label>目的地</label>
<input type="text" id="destination" value="上海" placeholder="输入城市">
</div>
<div class="form-group">
<label>预算(元/晚)</label>
<input type="number" id="budget" value="800" placeholder="如 800">
</div>
<div class="form-group">
<label>偏好</label>
<div class="checkboxes">
<label><input type="checkbox" value="海景"> 海景</label>
<label><input type="checkbox" value="免费早餐"> 免费早餐</label>
<label><input type="checkbox" value="健身房"> 健身房</label>
<label><input type="checkbox" value="商务中心"> 商务中心</label>
<label><input type="checkbox" value="泳池"> 泳池</label>
<label><input type="checkbox" value="SPA"> SPA</label>
</div>
</div>
<button id="searchBtn">🔍 智能推荐</button>
<div class="hotel-list" id="hotelList">
<p class="placeholder">点击推荐查看结果</p>
</div>
</aside>
<main class="map-container">
<div id="map"></div>
</main>
</div>
</div>
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<script src="static/js/main.js"></script>
</body>
</html>