Travel Service Mini Program Case
This case showcases a comprehensive travel service mini program that provides users with travel planning, booking services, local guides, and travel experiences, making travel more convenient and enjoyable.
Project Overview
Project Background
Modern travelers seek convenient, integrated solutions for planning and managing their trips. This mini program addresses the need for a one-stop travel platform that combines booking services, local recommendations, and travel management tools in a mobile-first experience.
Core Features
- Trip Planning: Itinerary creation and management tools
- Booking Services: Hotels, flights, and attraction tickets
- Local Guides: Curated recommendations and insider tips
- Real-time Updates: Flight status, weather, and travel alerts
- Expense Tracking: Travel budget management and expense recording
- Social Sharing: Travel moments and experience sharing
- Offline Maps: Downloadable maps and navigation
Technical Implementation
Trip Planning System
javascript
// pages/trip-planner/trip-planner.js
Page({
data: {
trip: {
title: '',
destination: '',
startDate: '',
endDate: '',
travelers: 1,
budget: 0,
preferences: []
},
destinations: [],
itinerary: [],
recommendations: [],
loading: false
},
onLoad(options) {
if (options.tripId) {
this.loadExistingTrip(options.tripId)
} else {
this.loadDestinations()
}
},
async loadDestinations() {
try {
const res = await wx.request({
url: '/api/travel/destinations/popular',
method: 'GET'
})
this.setData({
destinations: res.data.destinations
})
} catch (error) {
console.error('Failed to load destinations:', error)
}
},
async loadExistingTrip(tripId) {
try {
const res = await wx.request({
url: `/api/travel/trips/${tripId}`,
method: 'GET'
})
this.setData({
trip: res.data.trip,
itinerary: res.data.itinerary || []
})
this.loadRecommendations()
} catch (error) {
console.error('Failed to load trip:', error)
}
},
onTripInfoChange(e) {
const { field } = e.currentTarget.dataset
const { value } = e.detail
this.setData({
[`trip.${field}`]: value
})
// Auto-generate recommendations when destination changes
if (field === 'destination' && value) {
this.loadRecommendations()
}
},
onDateChange(e) {
const { type } = e.currentTarget.dataset
const { value } = e.detail
this.setData({
[`trip.${type}Date`]: value
})
this.calculateTripDuration()
},
calculateTripDuration() {
const { startDate, endDate } = this.data.trip
if (startDate && endDate) {
const start = new Date(startDate)
const end = new Date(endDate)
const duration = Math.ceil((end - start) / (1000 * 60 * 60 * 24))
this.setData({
'trip.duration': duration
})
}
},
async loadRecommendations() {
const { destination, startDate, endDate, travelers, preferences } = this.data.trip
if (!destination) return
try {
const res = await wx.request({
url: '/api/travel/recommendations',
method: 'POST',
data: {
destination,
startDate,
endDate,
travelers,
preferences
}
})
this.setData({
recommendations: res.data.recommendations
})
} catch (error) {
console.error('Failed to load recommendations:', error)
}
},
onAddToItinerary(e) {
const { recommendation } = e.currentTarget.dataset
const itinerary = [...this.data.itinerary]
// Find appropriate day to add the recommendation
const dayIndex = this.findBestDay(recommendation)
if (!itinerary[dayIndex]) {
itinerary[dayIndex] = {
day: dayIndex + 1,
date: this.calculateDate(dayIndex),
activities: []
}
}
itinerary[dayIndex].activities.push({
id: `activity_${Date.now()}`,
...recommendation,
startTime: this.suggestTime(itinerary[dayIndex].activities),
duration: recommendation.suggestedDuration || 120
})
this.setData({ itinerary })
this.optimizeItinerary()
},
findBestDay(recommendation) {
const { duration } = this.data.trip
if (!duration) return 0
// Simple algorithm to distribute activities across days
const activitiesPerDay = this.data.itinerary.map(day => day.activities.length)
const minActivities = Math.min(...activitiesPerDay, 0)
return activitiesPerDay.findIndex(count => count === minActivities) || 0
},
calculateDate(dayIndex) {
const startDate = new Date(this.data.trip.startDate)
const targetDate = new Date(startDate)
targetDate.setDate(startDate.getDate() + dayIndex)
return targetDate.toISOString().split('T')[0]
},
suggestTime(existingActivities) {
if (existingActivities.length === 0) return '09:00'
// Find the latest end time and suggest next available slot
const latestEndTime = existingActivities.reduce((latest, activity) => {
const endTime = this.addMinutes(activity.startTime, activity.duration)
return endTime > latest ? endTime : latest
}, '09:00')
return this.addMinutes(latestEndTime, 30) // 30 minutes buffer
},
addMinutes(time, minutes) {
const [hours, mins] = time.split(':').map(Number)
const totalMinutes = hours * 60 + mins + minutes
const newHours = Math.floor(totalMinutes / 60)
const newMins = totalMinutes % 60
return `${newHours.toString().padStart(2, '0')}:${newMins.toString().padStart(2, '0')}`
},
async optimizeItinerary() {
try {
const res = await wx.request({
url: '/api/travel/itinerary/optimize',
method: 'POST',
data: {
itinerary: this.data.itinerary,
destination: this.data.trip.destination
}
})
if (res.data.optimized) {
this.setData({
itinerary: res.data.itinerary
})
}
} catch (error) {
console.error('Failed to optimize itinerary:', error)
}
},
async saveTrip() {
const { trip, itinerary } = this.data
if (!trip.title || !trip.destination || !trip.startDate) {
wx.showToast({
title: 'Please fill required fields',
icon: 'error'
})
return
}
try {
wx.showLoading({ title: 'Saving trip...' })
const res = await wx.request({
url: '/api/travel/trips',
method: 'POST',
data: {
...trip,
itinerary,
userId: wx.getStorageSync('userId')
}
})
if (res.data.success) {
wx.showToast({
title: 'Trip saved successfully',
icon: 'success'
})
setTimeout(() => {
wx.redirectTo({
url: `/pages/trip-detail/trip-detail?id=${res.data.tripId}`
})
}, 1500)
}
} catch (error) {
console.error('Failed to save trip:', error)
wx.showToast({
title: 'Failed to save trip',
icon: 'error'
})
} finally {
wx.hideLoading()
}
},
onPreviewItinerary() {
wx.navigateTo({
url: '/pages/itinerary-preview/itinerary-preview',
success: (res) => {
res.eventChannel.emit('itineraryData', {
trip: this.data.trip,
itinerary: this.data.itinerary
})
}
})
}
})
Booking Integration System
javascript
// utils/booking-service.js
class BookingService {
static async searchFlights(origin, destination, departDate, returnDate, passengers) {
try {
const res = await wx.request({
url: '/api/travel/flights/search',
method: 'POST',
data: {
origin,
destination,
departDate,
returnDate,
passengers
}
})
return res.data.flights
} catch (error) {
console.error('Failed to search flights:', error)
return []
}
}
static async searchHotels(destination, checkIn, checkOut, guests, rooms) {
try {
const res = await wx.request({
url: '/api/travel/hotels/search',
method: 'POST',
data: {
destination,
checkIn,
checkOut,
guests,
rooms
}
})
return res.data.hotels
} catch (error) {
console.error('Failed to search hotels:', error)
return []
}
}
static async bookFlight(flightId, passengerInfo, paymentInfo) {
try {
const res = await wx.request({
url: '/api/travel/flights/book',
method: 'POST',
data: {
flightId,
passengerInfo,
paymentInfo,
userId: wx.getStorageSync('userId')
}
})
if (res.data.success) {
await this.processPayment(res.data.booking)
return res.data.booking
}
} catch (error) {
console.error('Failed to book flight:', error)
throw error
}
}
static async bookHotel(hotelId, roomId, guestInfo, paymentInfo) {
try {
const res = await wx.request({
url: '/api/travel/hotels/book',
method: 'POST',
data: {
hotelId,
roomId,
guestInfo,
paymentInfo,
userId: wx.getStorageSync('userId')
}
})
if (res.data.success) {
await this.processPayment(res.data.booking)
return res.data.booking
}
} catch (error) {
console.error('Failed to book hotel:', error)
throw error
}
}
static async processPayment(booking) {
try {
const paymentRes = await wx.request({
url: '/api/travel/payment/process',
method: 'POST',
data: {
bookingId: booking.id,
amount: booking.totalAmount,
currency: booking.currency
}
})
const paymentParams = paymentRes.data
return new Promise((resolve, reject) => {
wx.requestPayment({
...paymentParams,
success: (res) => {
this.confirmBooking(booking.id)
resolve(res)
},
fail: (error) => {
this.cancelBooking(booking.id)
reject(error)
}
})
})
} catch (error) {
console.error('Failed to process payment:', error)
throw error
}
}
static async confirmBooking(bookingId) {
try {
await wx.request({
url: `/api/travel/bookings/${bookingId}/confirm`,
method: 'PUT'
})
} catch (error) {
console.error('Failed to confirm booking:', error)
}
}
static async cancelBooking(bookingId) {
try {
await wx.request({
url: `/api/travel/bookings/${bookingId}/cancel`,
method: 'PUT'
})
} catch (error) {
console.error('Failed to cancel booking:', error)
}
}
static async getBookingDetails(bookingId) {
try {
const res = await wx.request({
url: `/api/travel/bookings/${bookingId}`,
method: 'GET'
})
return res.data.booking
} catch (error) {
console.error('Failed to get booking details:', error)
return null
}
}
static async getUserBookings(userId) {
try {
const res = await wx.request({
url: `/api/travel/users/${userId}/bookings`,
method: 'GET'
})
return res.data.bookings
} catch (error) {
console.error('Failed to get user bookings:', error)
return []
}
}
}
export default BookingService
Local Guide System
javascript
// pages/local-guide/local-guide.js
Page({
data: {
location: '',
guides: [],
categories: [
{ id: 'restaurants', name: 'Restaurants', icon: 'restaurant' },
{ id: 'attractions', name: 'Attractions', icon: 'location' },
{ id: 'shopping', name: 'Shopping', icon: 'shopping' },
{ id: 'nightlife', name: 'Nightlife', icon: 'moon' },
{ id: 'culture', name: 'Culture', icon: 'culture' },
{ id: 'nature', name: 'Nature', icon: 'tree' }
],
selectedCategory: 'restaurants',
nearbyPlaces: [],
userLocation: null,
loading: false
},
onLoad(options) {
this.setData({
location: options.location || ''
})
this.getCurrentLocation()
this.loadLocalGuides()
},
getCurrentLocation() {
wx.getLocation({
type: 'gcj02',
success: (res) => {
this.setData({
userLocation: {
latitude: res.latitude,
longitude: res.longitude
}
})
this.loadNearbyPlaces()
},
fail: (error) => {
console.error('Failed to get location:', error)
wx.showModal({
title: 'Location Access',
content: 'Please enable location access for better recommendations',
showCancel: false
})
}
})
},
async loadLocalGuides() {
try {
const res = await wx.request({
url: '/api/travel/local-guides',
method: 'GET',
data: {
location: this.data.location,
category: this.data.selectedCategory
}
})
this.setData({
guides: res.data.guides
})
} catch (error) {
console.error('Failed to load local guides:', error)
}
},
async loadNearbyPlaces() {
if (!this.data.userLocation) return
try {
this.setData({ loading: true })
const res = await wx.request({
url: '/api/travel/nearby-places',
method: 'GET',
data: {
latitude: this.data.userLocation.latitude,
longitude: this.data.userLocation.longitude,
category: this.data.selectedCategory,
radius: 5000 // 5km radius
}
})
this.setData({
nearbyPlaces: res.data.places,
loading: false
})
} catch (error) {
console.error('Failed to load nearby places:', error)
this.setData({ loading: false })
}
},
onCategorySelect(e) {
const categoryId = e.currentTarget.dataset.id
this.setData({
selectedCategory: categoryId
})
this.loadLocalGuides()
this.loadNearbyPlaces()
},
onPlaceDetail(e) {
const { placeId } = e.currentTarget.dataset
wx.navigateTo({
url: `/pages/place-detail/place-detail?id=${placeId}`
})
},
onGetDirections(e) {
const { place } = e.currentTarget.dataset
wx.openLocation({
latitude: place.latitude,
longitude: place.longitude,
name: place.name,
address: place.address,
scale: 18
})
},
async onAddToFavorites(e) {
const { placeId } = e.currentTarget.dataset
try {
const res = await wx.request({
url: '/api/travel/favorites',
method: 'POST',
data: {
placeId,
userId: wx.getStorageSync('userId')
}
})
if (res.data.success) {
wx.showToast({
title: 'Added to favorites',
icon: 'success'
})
}
} catch (error) {
console.error('Failed to add to favorites:', error)
wx.showToast({
title: 'Failed to add favorite',
icon: 'error'
})
}
},
onSharePlace(e) {
const { place } = e.currentTarget.dataset
return {
title: place.name,
desc: place.description,
path: `/pages/place-detail/place-detail?id=${place.id}`,
imageUrl: place.images[0]
}
},
onSearchPlaces() {
wx.navigateTo({
url: '/pages/place-search/place-search'
})
}
})
Travel Expense Tracker
javascript
// utils/expense-tracker.js
class ExpenseTracker {
static async addExpense(tripId, expense) {
try {
const res = await wx.request({
url: '/api/travel/expenses',
method: 'POST',
data: {
tripId,
...expense,
userId: wx.getStorageSync('userId'),
timestamp: Date.now()
}
})
return res.data.expense
} catch (error) {
console.error('Failed to add expense:', error)
throw error
}
}
static async getExpenses(tripId) {
try {
const res = await wx.request({
url: `/api/travel/trips/${tripId}/expenses`,
method: 'GET'
})
return res.data.expenses
} catch (error) {
console.error('Failed to get expenses:', error)
return []
}
}
static async updateExpense(expenseId, updates) {
try {
const res = await wx.request({
url: `/api/travel/expenses/${expenseId}`,
method: 'PUT',
data: updates
})
return res.data.expense
} catch (error) {
console.error('Failed to update expense:', error)
throw error
}
}
static async deleteExpense(expenseId) {
try {
await wx.request({
url: `/api/travel/expenses/${expenseId}`,
method: 'DELETE'
})
return true
} catch (error) {
console.error('Failed to delete expense:', error)
throw error
}
}
static calculateExpenseSummary(expenses) {
const summary = {
total: 0,
categories: {},
daily: {},
currency: 'CNY'
}
expenses.forEach(expense => {
summary.total += expense.amount
// Group by category
if (!summary.categories[expense.category]) {
summary.categories[expense.category] = 0
}
summary.categories[expense.category] += expense.amount
// Group by date
const date = expense.date || new Date(expense.timestamp).toISOString().split('T')[0]
if (!summary.daily[date]) {
summary.daily[date] = 0
}
summary.daily[date] += expense.amount
})
return summary
}
static async generateExpenseReport(tripId, format = 'pdf') {
try {
const res = await wx.request({
url: `/api/travel/trips/${tripId}/expense-report`,
method: 'POST',
data: {
format,
userId: wx.getStorageSync('userId')
}
})
return res.data.reportUrl
} catch (error) {
console.error('Failed to generate expense report:', error)
throw error
}
}
static async splitExpense(expenseId, splitData) {
try {
const res = await wx.request({
url: `/api/travel/expenses/${expenseId}/split`,
method: 'POST',
data: splitData
})
return res.data.splits
} catch (error) {
console.error('Failed to split expense:', error)
throw error
}
}
static convertCurrency(amount, fromCurrency, toCurrency, exchangeRates) {
if (fromCurrency === toCurrency) return amount
const rate = exchangeRates[`${fromCurrency}_${toCurrency}`]
return rate ? amount * rate : amount
}
static async getExchangeRates(baseCurrency = 'CNY') {
try {
const res = await wx.request({
url: '/api/travel/exchange-rates',
method: 'GET',
data: { baseCurrency }
})
return res.data.rates
} catch (error) {
console.error('Failed to get exchange rates:', error)
return {}
}
}
}
export default ExpenseTracker
Travel Social Features
javascript
// pages/travel-moments/travel-moments.js
Page({
data: {
moments: [],
userMoments: [],
loading: false,
showCreateForm: false,
newMoment: {
content: '',
images: [],
location: '',
tags: []
}
},
onLoad() {
this.loadTravelMoments()
this.loadUserMoments()
},
async loadTravelMoments() {
try {
const res = await wx.request({
url: '/api/travel/moments',
method: 'GET',
data: {
limit: 20,
offset: 0
}
})
this.setData({
moments: res.data.moments
})
} catch (error) {
console.error('Failed to load travel moments:', error)
}
},
async loadUserMoments() {
try {
const res = await wx.request({
url: '/api/travel/moments/user',
method: 'GET',
data: {
userId: wx.getStorageSync('userId')
}
})
this.setData({
userMoments: res.data.moments
})
} catch (error) {
console.error('Failed to load user moments:', error)
}
},
onShowCreateForm() {
this.setData({ showCreateForm: true })
},
onHideCreateForm() {
this.setData({
showCreateForm: false,
newMoment: {
content: '',
images: [],
location: '',
tags: []
}
})
},
onContentChange(e) {
this.setData({
'newMoment.content': e.detail.value
})
},
onChooseImages() {
wx.chooseImage({
count: 9 - this.data.newMoment.images.length,
sizeType: ['compressed'],
sourceType: ['album', 'camera'],
success: (res) => {
const images = [...this.data.newMoment.images, ...res.tempFilePaths]
this.setData({
'newMoment.images': images
})
}
})
},
onRemoveImage(e) {
const { index } = e.currentTarget.dataset
const images = [...this.data.newMoment.images]
images.splice(index, 1)
this.setData({
'newMoment.images': images
})
},
onChooseLocation() {
wx.chooseLocation({
success: (res) => {
this.setData({
'newMoment.location': res.name
})
}
})
},
async onPublishMoment() {
const { newMoment } = this.data
if (!newMoment.content.trim()) {
wx.showToast({
title: 'Please enter content',
icon: 'error'
})
return
}
try {
wx.showLoading({ title: 'Publishing...' })
// Upload images first
const imageUrls = await this.uploadImages(newMoment.images)
const res = await wx.request({
url: '/api/travel/moments',
method: 'POST',
data: {
...newMoment,
images: imageUrls,
userId: wx.getStorageSync('userId')
}
})
if (res.data.success) {
wx.showToast({
title: 'Published successfully',
icon: 'success'
})
this.onHideCreateForm()
this.loadTravelMoments()
this.loadUserMoments()
}
} catch (error) {
console.error('Failed to publish moment:', error)
wx.showToast({
title: 'Publish failed',
icon: 'error'
})
} finally {
wx.hideLoading()
}
},
async uploadImages(imagePaths) {
const uploadPromises = imagePaths.map(path =>
new Promise((resolve, reject) => {
wx.uploadFile({
url: '/api/travel/upload/image',
filePath: path,
name: 'image',
success: (res) => {
const data = JSON.parse(res.data)
resolve(data.imageUrl)
},
fail: reject
})
})
)
return Promise.all(uploadPromises)
},
async onLikeMoment(e) {
const { momentId } = e.currentTarget.dataset
try {
const res = await wx.request({
url: `/api/travel/moments/${momentId}/like`,
method: 'POST',
data: {
userId: wx.getStorageSync('userId')
}
})
if (res.data.success) {
// Update local data
this.updateMomentLikes(momentId, res.data.liked)
}
} catch (error) {
console.error('Failed to like moment:', error)
}
},
updateMomentLikes(momentId, liked) {
const moments = this.data.moments.map(moment => {
if (moment.id === momentId) {
return {
...moment,
liked,
likesCount: liked ? moment.likesCount + 1 : moment.likesCount - 1
}
}
return moment
})
this.setData({ moments })
},
onCommentMoment(e) {
const { momentId } = e.currentTarget.dataset
wx.navigateTo({
url: `/pages/moment-comments/moment-comments?momentId=${momentId}`
})
},
onShareMoment(e) {
const { moment } = e.currentTarget.dataset
return {
title: 'Travel Moment',
desc: moment.content,
path: `/pages/moment-detail/moment-detail?id=${moment.id}`,
imageUrl: moment.images[0]
}
}
})
Project Results
Key Metrics
- User Engagement: 78% of users complete their planned itineraries
- Booking Conversion: 65% conversion rate from planning to booking
- User Satisfaction: 4.6/5.0 average rating for travel experience
- Cost Savings: 25% average savings through integrated booking
- Social Engagement: 45% of users share travel moments
Business Impact
- Revenue Growth: 40% increase in travel bookings through the platform
- User Retention: 70% of users return for subsequent trips
- Market Expansion: 35% growth in new destination bookings
- Operational Efficiency: 50% reduction in customer service inquiries
- Partner Network: 200+ integrated travel service providers
This travel service mini program successfully demonstrates how comprehensive travel platforms can enhance the travel experience, from initial planning to sharing memories, while driving business growth through integrated services and social features.