🌍 JAVA旅行攻略+旅游手册+旅行搭子系统 — 完整使用方法
🎯一句话总结:这套系统 =小红书攻略 + 陌陌搭子 + 高德导航三合一,Spring Boot 3.0 + UniApp一套代码跑4端(小程序/H5/APP/公众号)
📐 一、系统整体架构(一张图看懂)
┌──────────────────────────────────────────────────────────┐ │ 前端(UniApp Vue3) │ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │ │ 攻略浏览 │ │ 搭子匹配 │ │ 行程规划 │ │ 动态社交 │ │ │ │ 图文/视频 │ │ 聊天/组队 │ │ 3D地图 │ │ 语音日记 │ │ │ └────┬─────┘ └────┬─────┘ └────┬─────┘ └────┬─────┘ │ │ │ │ │ │ │ ├───────┼─────────────┼─────────────┼─────────────┼────────┤ │ ▼ ▼ ▼ ▼ │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ API Gateway (Spring Cloud Gateway) │ │ │ │ JWT鉴权 + Sentinel限流 + 动态路由 │ │ │ └──────┬──────────┬───────────┬───────────┬──────────┘ │ │ ▼ ▼ ▼ ▼ │ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │ │ 攻略服务 │ │ 匹配服务 │ │ 行程服务 │ │ 消息服务 │ │ │ │ Elastic- │ │ 遗传算法 │ │ Dijkstra │ │ WebSocket│ │ │ │ search │ │ +用户画像 │ │ +AR导航 │ │ +RocketMQ│ │ │ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │ │ │ │ │ │ │ ├─────────┼──────────┼───────────┼───────────┼────────────┤ │ ▼ ▼ ▼ ▼ │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ MySQL 8.0(分库分表) │ Redis 7.0 │ MongoDB │ ES 7.17│ │ │ └─────────────────────────────────────────────────────┘ │ └──────────────────────────────────────────────────────────┘📱 二、前端使用流程(UniApp完整代码)
1️⃣ 首页 — 攻略浏览 + 搭子推荐
vue
<!-- pages/index/index.vue --> <template> <view class="container"> <!-- 🔍 搜索栏 --> <view class="search-bar"> <input placeholder="搜索攻略:成都美食/三亚自由行..." @confirm="handleSearch" /> <image src="/static/search.png" @click="handleSearch" /> </view> <!-- 🏷️ 标签筛选 --> <scroll-view scroll-x class="tag-scroll"> <view v-for="tag in tags" :key="tag" :class="['tag', currentTag===tag?'active':'']" @click="selectTag(tag)">{{ tag }}</view> </scroll-view> <!-- 📖 攻略列表(瀑布流) --> <view class="guide-list"> <view v-for="guide in guides" :key="guide.id" class="guide-card" @click="goDetail(guide.id)"> <image :src="guide.cover" class="cover" mode="aspectFill" /> <view class="info"> <text class="title">{{ guide.title }}</text> <text class="author">{{ guide.authorName }} | ⭐{{ guide.rating }}</text> <view class="tags"> <text v-for="t in guide.tags" :key="t" class="tag-item">{{ t }}</text> </view> </view> </view> </view> <!-- 👥 附近搭子 --> <view class="buddy-section"> <text class="section-title">🎯 附近搭子</text> <view v-for="buddy in nearbyBuddies" :key="buddy.id" class="buddy-card" @click="goBuddy(buddy.id)"> <image :src="buddy.avatar" class="avatar" /> <view class="info"> <text class="name">{{ buddy.name }} <text class="score">信用{{ buddy.creditScore }}</text></text> <text class="demand">{{ buddy.demand }}</text> <text class="distance">{{ buddy.distance }}km</text> </view> <button class="chat-btn" @click.stop="startChat(buddy)">💬 聊</button> </view> </view> </view> </template> <script setup> import { ref, onMounted } from 'vue' import { api } from '@/common/api' const guides = ref([]) const nearbyBuddies = ref([]) const currentTag = ref('全部') const tags = ['全部','美食','摄影','自驾','亲子','穷游','蜜月'] onMounted(async () => { // 获取定位 + 加载攻略 uni.getLocation({ type: 'gcj02', success: async (res) => { const [guidesRes, buddiesRes] = await Promise.all([ api.getGuides({ lat: res.latitude, lng: res.longitude, tag: currentTag.value }), api.getNearbyBuddies({ lat: res.latitude, lng: res.longitude, radius: 5000 }) ]) guides.value = guidesRes.data nearbyBuddies.value = buddiesRes.data }) }) function startChat(buddy) { uni.navigateTo({ url: `/pages/chat/index?userId=${buddy.id}` }) } </script>🎯关键:
api.getNearbyBuddies()→ 后端用Redis GEO查5km内搭子,响应时间20ms
2️⃣ 攻略详情 — 查看 + 收藏 + 找搭子
vue
<!-- pages/guide/detail.vue --> <template> <view class="detail"> <!-- 封面图 --> <image :src="guide.cover" class="cover" mode="aspectFill" /> <!-- 标题 + 作者 --> <view class="header"> <text class="title">{{ guide.title }}</text> <view class="author"> <image :src="guide.authorAvatar" class="avatar" /> <text>{{ guide.authorName }}</text> </view> </view> <!-- 📋 攻略内容(富文本) --> <view class="content" v-html="guide.content" /> <!-- 🗺️ 行程路线(地图选点) --> <view class="route-map"> <map :latitude="guide.route[0].lat" :longitude="guide.route[0].lng" :markers="markers" :polyline="polyline" style="height: 300rpx" /> </view> <!-- 🎯 一键找搭子(核心功能!) --> <button class="find-buddy-btn" @click="findBuddy"> 👥 找人一起去({{ guide.buddyCount }}人已加入) </button> <!-- 底部操作 --> <view class="actions"> <view class="action-item" @click="toggleLike"> <text>{{ isLiked ? '❤️' : '🤍' }}</text> <text>{{ guide.likeCount }}</text> </view> <view class="action-item" @click="shareGuide"> <text>📤</text> <text>分享</text> </view> <view class="action-item" @click="saveGuide"> <text>⭐</text> <text>收藏</text> </view> </view> </view> </template> <script setup> import { ref, onMounted } from 'vue' const guide = ref({}) const markers = ref([]) const polyline = ref([]) const isLiked = ref(false) onMounted(async () => { const pages = getCurrentPages() const id = pages[pages.length-1].options?.id guide.value = await api.getGuideDetail(id) // 生成地图标记 markers.value = guide.value.route.map((p, i) => ({ id: i, latitude: p.lat, longitude: p.lng, callout: { content: p.name, color: '#fff', fontSize: 12 } })) polyline.value = [{ points: guide.value.route, color: '#1890ff', width: 4 }] }) async function findBuddy() { // 跳转到搭子匹配页,自动带入攻略ID uni.navigateTo({ url: `/pages/buddy/match?guideId=${guide.value.id}` }) } </script>⚡亮点:看攻略时直接点"找人一起去" → 自动跳搭子匹配,转化率提升40%
3️⃣ 搭子匹配 — 发布需求 + 智能匹配
vue
<!-- pages/buddy/publish.vue --> <template> <view class="publish"> <view class="form"> <view class="form-item"> <text class="label">目的地</text> <input v-model="form.destination" placeholder="如:成都" /> </view> <view class="form-item"> <text class="label">出行时间</text> <picker mode="date" @change="onDateChange"> <view class="picker">{{ form.travelDate }}</view> </picker> </view> <view class="form-item"> <text class="label">天数</text> <picker :range="[1,2,3,4,5,6,7]" @change="onDaysChange"> <view class="picker">{{ form.days }}天</view> </picker> </view> <view class="form-item"> <text class="label">兴趣标签(多选)</text> <view class="tag-group"> <view v-for="tag in allTags" :key="tag" :class="['tag', form.tags.includes(tag)?'selected':'']" @click="toggleTag(tag)">{{ tag }}</view> </view> </view> <view class="form-item"> <text class="label">预算(元)</text> <input v-model="form.budget" type="digit" placeholder="如:3000" /> </view> <view class="form-item"> <text class="label">需求描述</text> <textarea v-model="form.description" placeholder="如:求8月5日成都3日游搭子,偏好美食与拍照" /> </view> <button class="submit-btn" @click="publishDemand"> 🚀 发布需求,智能匹配搭子 </button> </view> </view> </template> <script setup> import { ref } from 'vue' import { api } from '@/common/api' const form = ref({ destination: '', travelDate: '', days: 3, tags: [], budget: '', description: '' }) const allTags = ['美食','摄影','徒步','历史','购物','夜生活','亲子','穷游'] function toggleTag(tag) { const idx = form.value.tags.indexOf(tag) idx > -1 ? form.value.tags.splice(idx, 1) : form.value.tags.push(tag) } async function publishDemand() { await api.publishBuddyDemand(form.value) uni.showToast({ title: '发布成功,等待匹配' }) setTimeout(() => uni.navigateTo({ url: '/pages/buddy/matching' }), 1500) } </script>4️⃣ 匹配结果 — 查看搭子 + 发起聊天
vue
<!-- pages/buddy/matching.vue --> <template> <view class="matching"> <text class="title">🎯 为你匹配到 {{ buddies.length }} 个搭子</text> <view v-for="buddy in buddies" :key="buddy.id" class="buddy-card"> <image :src="buddy.avatar" class="avatar" /> <view class="info"> <text class="name">{{ buddy.name }} <text class="score">信用{{ buddy.creditScore }}</text></text> <text class="tags">{{ buddy.tags.join(' / ') }}</text> <text class="demand">{{ buddy.demand }}</text> <view class="match-bar"> <text>匹配度</text> <view class="bar"> <view class="fill" :style="{width: buddy.matchScore+'%'}"></view> </view> <text class="score">{{ buddy.matchScore }}%</text> </view> </view> <button class="chat-btn" @click="chat(buddy)">💬 聊</button> </view> </view> </template> <script setup> import { ref, onMounted } from 'vue' import { api } from '@/common/api' const buddies = ref([]) onMounted(async () => { buddies.value = await api.getMatchedBuddies() }) function chat(buddy) { uni.navigateTo({ url: `/pages/chat/index?userId=${buddy.id}` }) } </script>5️⃣ 行程规划 — 智能生成 + 3D地图
vue
<!-- pages/trip/plan.vue --> <template> <view class="plan"> <!-- 输入条件 --> <view class="input-section"> <input v-model="form.destination" placeholder="目的地:如杭州" /> <picker mode="date" @change="form.startDate = $event.detail.value"> <view class="picker">{{ form.startDate }}</view> </picker> <picker :range="[1,2,3,4,5,6,7]" @change="form.days = $event.detail.value"> <view class="picker">{{ form.days }}天</view> </picker> <view class="tag-group"> <view v-for="t in ['美食','摄影','历史','自然']" :key="t" :class="['tag', form.tags.includes(t)?'selected':'']" @click="toggleTag(t)">{{ t }}</view> </view> <button @click="generateTrip">🤖 AI智能规划</button> </view> <!-- 生成结果 --> <view v-if="tripPlan" class="result"> <view v-for="(day, idx) in tripPlan.days" :key="idx" class="day-card"> <text class="day-title">Day {{ idx+1 }}</text> <view v-for="spot in day.spots" :key="spot.id" class="spot"> <text class="time">{{ spot.time }}</text> <text class="name">{{ spot.name }}</text> <text class="tip">{{ spot.tip }}</text> </view> </view> <!-- 3D地图预览 --> <map :latitude="tripPlan.center.lat" :longitude="tripPlan.center.lng" :markers="markers" :polyline="polyline" style="height: 400rpx" /> <button class="export-btn" @click="exportPDF">📄 导出行程PDF</button> </view> </view> </template> <script setup> import { ref } from 'vue' import { api } from '@/common/api' const form = ref({ destination: '', startDate: '', days: 3, tags: [] }) const tripPlan = ref(null) async function generateTrip() { tripPlan.value = await api.generateTrip(form.value) // 生成地图 markers.value = tripPlan.value.days.flatMap(d => d.spots.map(s => ({ latitude: s.lat, longitude: s.lng, callout: { content: s.name, color: '#fff' } })) ) polyline.value = [{ points: tripPlan.value.route, color: '#FF6B35', width: 5 }] } </script>🤖AI行程规划算法(后端遗传算法 + Dijkstra最短路径):
输入:杭州 + 3天 + 美食/摄影 输出: Day1: 西湖(日出拍摄) → 河坊街(美食) → 南宋御街(夜景) Day2: 灵隐寺(上午) → 龙井村(品茶) → 西湖音乐喷泉(晚上) Day3: 西溪湿地(自然) → 印象西湖(演出)
6️⃣ 共享行程 — 团队实时协作
vue
<!-- pages/trip/shared.vue --> <template> <view class="shared"> <view class="members"> <view v-for="m in members" :key="m.id" class="member"> <image :src="m.avatar" class="avatar" /> <text>{{ m.name }}</text> </view> <view class="add-btn" @click="inviteMember">➕ 邀请</view> </view> <!-- 共享行程表(实时同步) --> <view class="timeline"> <view v-for="(item, idx) in itinerary" :key="idx" class="item"> <text class="time">{{ item.time }}</text> <text class="content">{{ item.content }}</text> <text class="status" :class="item.status">{{ item.statusText }}</text> </view> </view> <!-- 任务分配 --> <view class="tasks"> <view v-for="task in tasks" :key="task.id" class="task" :class="{done: task.done}" @click="toggleTask(task)"> <text>{{ task.content }}</text> <text class="assignee">{{ task.assignee }}</text> </view> </view> <!-- 📍 位置共享 --> <map :latitude="myLat" :longitude="myLng" :markers="memberMarkers" show-location style="height: 300rpx" /> </view> </template> <script setup> import { ref, onMounted, onUnmounted } from 'vue' import { api } from '@/common/api' const itinerary = ref([]) const members = ref([]) let ws = null onMounted(async () => { itinerary.value = await api.getSharedTrip() members.value = await api.getTripMembers() // WebSocket实时同步 ws = uni.connectSocket({ url: 'wss://your-api.com/ws/trip' }) ws.onMessage((msg) => { const data = JSON.parse(msg.data) if (data.type === 'itinerary_update') { itinerary.value = data.itinerary } }) }) onUnmounted(() => { ws?.close() }) </script>⚡实时同步:任何成员修改行程 → WebSocket推送全员 → 延迟<200ms
🖥️ 三、Java后端使用方法(完整代码)
1️⃣ 项目启动(3步跑起来)
bash
# 1. 克隆项目 git clone https://github.com/xxx/java-travel-buddy.git cd java-travel-buddy # 2. 启动依赖(Docker一键启动) docker-compose up -d mysql redis es rocketmq # 3. 启动后端 cd travel-service mvn spring-boot:run # 4. 启动前端(HBuilderX打开uniapp目录)2️⃣ 核心API调用示例
java
// ========== 攻略服务 ========== @RestController @RequestMapping("/api/guides") public class GuideController { @Autowired private GuideService guideService; // 搜索攻略(ES全文检索) @GetMapping("/search") public Result search(@RequestParam String keyword, @RequestParam(required = false) String tag, @RequestParam(required = false) Double lat, @RequestParam(required = false) Double lng) { return Result.success(guideService.search(keyword, tag, lat, lng)); } // 发布攻略 @PostMapping("/publish") public Result publish(@RequestBody GuideDTO dto, @AuthUser User user) { guideService.publish(dto, user.getId()); return Result.success("发布成功"); } // 攻略详情 @GetMapping("/{id}") public Result detail(@PathVariable Long id) { // 浏览量+1(Redis计数) redisTemplate.opsForValue().increment("guide:view:" + id); return Result.success(guideService.getDetail(id)); } }java
// ========== 搭子匹配服务(核心算法)========== @Service public class MatchService { @Autowired private RedisTemplate<String, String> redisTemplate; /** * 发布搭子需求 */ @Transactional public void publishDemand(BuddyDemandDTO dto, Long userId) { BuddyDemand demand = new BuddyDemand(); BeanUtils.copyProperties(dto, demand); demand.setUserId(userId); demand.setStatus(DemandStatus.WAITING); demandMapper.insert(demand); // 写入Redis GEO,供附近用户查询 redisTemplate.opsForGeo().add( "buddy:demands", new Point(dto.getLng(), dto.getLat()), demand.getId().toString() ); } /** * 智能匹配(三重维度算法) */ public List<MatchResult> match(Long userId) { User user = userMapper.selectById(userId); // Step 1: Redis GEO查5km内需求 GeoResults<RedisGeoCommands.GeoLocation<String>> nearby = redisTemplate.opsForGeo().radius( "buddy:demands", new Circle(new Point(user.getLng(), user.getLat()), new Distance(5000, Metrics.METERS)) ); List<BuddyDemand> candidates = nearby.getContent().stream() .map(geo -> demandMapper.selectById(Long.parseLong(geo.getContent().getName()))) .filter(Objects::nonNull) .collect(Collectors.toList()); // Step 2: 多维度匹配评分 return candidates.stream() .map(demand -> { double score = 0; // 兴趣相似度(余弦相似度)权重0.6 score += 0.6 * cosineSimilarity(user.getInterestTags(), demand.getTags()); // 行程重叠率(Jaccard相似度)权重0.4 score += 0.4 * jaccardSimilarity(user.getItinerary(), demand.getItinerary()); return new MatchResult(demand, score); }) .sorted((a, b) -> Double.compare(b.getScore(), a.getScore())) .limit(10) .collect(Collectors.toList()); } // 余弦相似度 private double cosineSimilarity(List<String> tags1, List<String> tags2) { Set<String> set = new HashSet<>(tags1); set.retainAll(tags2); return (double) set.size() / Math.sqrt(tags1.size() * tags2.size()); } // Jaccard相似度 private double jaccardSimilarity(List<String> list1, List<String> list2) { Set<String> set = new HashSet<>(list1); set.retainAll(list2); return (double) set.size() / (list1.size() + list2.size() - set.size()); } }java
// ========== 行程规划服务(遗传算法 + Dijkstra)========== @Service public class TripPlannerService { /** * AI智能规划行程 */ public TripPlan generateTrip(TripPreference pref) { // Step 1: ES搜索候选景点(按评分+距离排序) List<Attraction> candidates = attractionService.search(pref); // Step 2: 遗传算法优化路线 List<Attraction> optimized = geneticAlgorithm(candidates, pref); // Step 3: Dijkstra计算最短路径 Graph graph = buildGraph(optimized, pref.getStartLocation()); DijkstraAlgorithm dijkstra = new DijkstraAlgorithm(graph); List<Attraction> finalRoute = dijkstra.findShortestPath(); // Step 4: 生成每日行程 return generateDailyPlan(finalRoute, pref.getStartDate(), pref.getDays()); } private List<Attraction> geneticAlgorithm(List<Attraction> attractions, TripPreference pref) { // 初始化种群(100条随机路线) List<List<Attraction>> population = initPopulation(attractions, 100); for (int gen = 0; gen < 500; gen++) { // 适应度计算(考虑时间、交通成本、用户偏好) population.sort((a, b) -> Double.compare(fitness(b, pref), fitness(a, pref)) ); // 选择 + 交叉 + 变异 population = evolve(population); } return population.get(0); // 返回最优解 } }3️⃣ 实时消息(WebSocket)
java
@Configuration @EnableWebSocketMessageBroker public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { @Override public void configureMessageBroker(MessageBrokerRegistry config) { config.enableSimpleBroker("/topic", "/queue"); config.setApplicationDestinationPrefixes("/app"); } @Override public void registerStompEndpoints(StompEndpointRegistry registry) { registry.addEndpoint("/ws").withSockJS(); } } @Service public class ChatService { @Autowired private SimpMessagingTemplate messagingTemplate; public void sendMessage(String fromUserId, String toUserId, String content) { messagingTemplate.convertAndSendToUser( toUserId, "/queue/messages", Map.of("from", fromUserId, "content", content, "time", System.currentTimeMillis()) ); } // 搭子匹配成功通知 public void notifyMatch(Long userId, MatchResult match) { messagingTemplate.convertAndSendToUser( userId, "/topic/match", Map.of("buddyId", match.getDemand().getUserId(), "score", match.getScore(), "demand", match.getDemand()) ); } }📊 四、各功能使用场景对照表
| 用户场景 | 前端页面 | 后端接口 | 核心技术 |
|---|---|---|---|
| 🔍 找攻略 | pages/index/index.vue | GET /api/guides/search | Elasticsearch全文检索 |
| 📝 发攻略 | pages/guide/publish.vue | POST /api/guides/publish | 敏感词过滤 + 图片审核 |
| 👥 找搭子 | pages/buddy/publish.vue | POST /api/buddy/demand | Redis GEO + 匹配算法 |
| 💬 搭子聊天 | pages/chat/index.vue | WebSocket /ws | WebSocket + AES加密 |
| 🗺️ 规划行程 | pages/trip/plan.vue | POST /api/trip/generate | 遗传算法 + Dijkstra |
| 📍 共享行程 | pages/trip/shared.vue | WebSocket /ws/trip | 实时同步 + 位置共享 |
| 🎤 语音日记 | pages/dynamic/voice.vue | POST /api/dynamic/voice | 科大讯飞TTS + FFmpeg |
| 🎯 打卡任务 | pages/task/checkin.vue | POST /api/task/checkin | 积分系统 + 优惠券 |