Skip to content

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.

Connecting Multiple Platforms, Empowering Innovation