🌏 JAVA旅游系统 + UniApp微信小程序源码示例

📋 系统架构


后端: Spring Boot + MyBatis + MySQL
前端: UniApp (Vue3) → 微信小程序

🔧 一、后端核心代码 (Java Spring Boot)

1. 实体类 - 旅游线路


java

@Data
@TableName("travel_route")
public class TravelRoute {
    @TableId(type = IdType.AUTO)
    private Long id;
    private String title;          // 线路标题
    private String coverImg;       // 封面图
    private String description;    // 描述
    private BigDecimal price;      // 价格
    private Integer days;          // 天数
    private String departure;      // 出发地
    private String destination;    // 目的地
    private Integer status;        // 0-下架 1-上架
    private LocalDateTime createTime;
}

2. Controller - 旅游接口


java

@RestController
@RequestMapping("/api/travel")
@CrossOrigin
public class TravelController {

    @Autowired
    private TravelService travelService;

    // 首页 - 轮播图 + 热门线路
    @GetMapping("/home")
    public Result home() {
        List<TravelRoute> hotRoutes = travelService.getHotRoutes();
        List<Banner> banners = travelService.getBanners();
        return Result.success(Map.of(
            "banners", banners,
            "hotRoutes", hotRoutes
        ));
    }

    // 线路列表
    @GetMapping("/list")
    public Result list(
        @RequestParam(defaultValue = "1") Integer page,
        @RequestParam(defaultValue = "10") Integer size,
        @RequestParam(required = false) String keyword
    ) {
        Page<TravelRoute> result = travelService.getPage(page, size, keyword);
        return Result.success(result);
    }

    // 线路详情
    @GetMapping("/detail/{id}")
    public Result detail(@PathVariable Long id) {
        TravelRoute route = travelService.getById(id);
        return Result.success(route);
    }

    // 下单
    @PostMapping("/order")
    public Result order(@RequestBody OrderDTO orderDTO) {
        String orderNo = travelService.createOrder(orderDTO);
        return Result.success(orderNo);
    }

    // 我的订单
    @GetMapping("/myOrders")
    public Result myOrders(@RequestAttribute Long userId) {
        List<Order> orders = travelService.getUserOrders(userId);
        return Result.success(orders);
    }
}

3. Service层


java

@Service
public class TravelServiceImpl implements TravelService {

    @Autowired
    private TravelRouteMapper routeMapper;

    @Override
    public Page<TravelRoute> getPage(Integer page, Integer size, String keyword) {
        LambdaQueryWrapper<TravelRoute> wrapper = new LambdaQueryWrapper<>();
        wrapper.like(StringUtils.isNotBlank(keyword), 
                     TravelRoute::getTitle, keyword)
               .eq(TravelRoute::getStatus, 1)
               .orderByDesc(TravelRoute::getCreateTime);
        return routeMapper.selectPage(
            new Page<>(page, size), wrapper
        );
    }

    @Override
    public String createOrder(OrderDTO dto) {
        String orderNo = "TX" + System.currentTimeMillis();
        Order order = new Order();
        order.setOrderNo(orderNo);
        order.setUserId(dto.getUserId());
        order.setRouteId(dto.getRouteId());
        order.setPrice(dto.getPrice());
        order.setStatus(0); // 待支付
        orderMapper.insert(order);
        return orderNo;
    }
}

📱 二、UniApp 前端代码 (微信小程序)

📁 项目结构


├── pages/
│   ├── index/          # 首页
│   ├── list/           # 线路列表
│   ├── detail/         # 线路详情
│   ├── order/          # 订单确认
│   └── mine/           # 我的
├── components/
│   └── nav-bar.vue     # 自定义导航栏
├── static/
├── pages.json
├── manifest.json
└── App.vue

1️⃣ pages.json - 页面配置


json

{
  "pages": [
    {
      "path": "pages/index/index",
      "style": {
        "navigationBarTitleText": "畅享旅游",
        "navigationStyle": "custom"
      }
    },
    {
      "path": "pages/list/list",
      "style": {
        "navigationBarTitleText": "旅游线路"
      }
    },
    {
      "path": "pages/detail/detail",
      "style": {
        "navigationBarTitleText": "线路详情"
      }
    },
    {
      "path": "pages/order/order",
      "style": {
        "navigationBarTitleText": "确认订单"
      }
    },
    {
      "path": "pages/mine/mine",
      "style": {
        "navigationBarTitleText": "我的"
      }
    }
  ],
  "tabBar": {
    "color": "#999",
    "selectedColor": "#07c160",
    "list": [
      {
        "pagePath": "pages/index/index",
        "text": "首页",
        "iconPath": "static/tab/home.png",
        "selectedIconPath": "static/tab/home-active.png"
      },
      {
        "pagePath": "pages/list/list",
        "text": "线路",
        "iconPath": "static/tab/list.png",
        "selectedIconPath": "static/tab/list-active.png"
      },
      {
        "pagePath": "pages/mine/mine",
        "text": "我的",
        "iconPath": "static/tab/mine.png",
        "selectedIconPath": "static/tab/mine-active.png"
      }
    ]
  },
  "globalStyle": {
    "navigationBarTextStyle": "black",
    "navigationBarBackgroundColor": "#fff",
    "backgroundColor": "#f5f5f5"
  }
}

2️⃣ App.vue - 全局配置


vue

<script>
export default {
  onLaunch() {
    console.log('App Launch')
    // 获取用户信息
    const userInfo = uni.getStorageSync('userInfo')
    if (userInfo) {
      this.$store.commit('SET_USER', userInfo)
    }
  },
  globalStyle: {
    navigationBarTextStyle: 'black',
    navigationBarTitleText: '畅享旅游',
    navigationBarBackgroundColor: '#FFF',
    backgroundColor: '#F8F8F8'
  }
}
</script>

<style>
/* 全局样式 */
page {
  background-color: #f5f5f5;
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}

.container {
  padding: 20rpx;
}

.price {
  color: #ff4d4f;
  font-weight: bold;
}

.btn-primary {
  background: linear-gradient(135deg, #07c160, #06ad56);
  color: #fff;
  border-radius: 50rpx;
  text-align: center;
  padding: 24rpx 0;
  font-size: 32rpx;
}
</style>

3️⃣ pages/index/index.vue - 首页 ⭐


vue

<template>
  <view class="index-page">
    <!-- 搜索栏 -->
    <view class="search-bar">
      <view class="search-input" @click="goSearch">
        <uni-icons type="search" size="18" color="#999"></uni-icons>
        <text class="placeholder">搜索目的地、线路...</text>
      </view>
    </view>

    <!-- 轮播图 -->
    <swiper class="banner" indicator-dots autoplay circular interval="3000">
      <swiper-item v-for="item in banners" :key="item.id">
        <image :src="item.imageUrl" mode="aspectFill" 
               class="banner-img" @click="goDetail(item.routeId)"></image>
      </swiper-item>
    </swiper>

    <!-- 快捷分类 -->
    <view class="category-grid">
      <view class="cat-item" v-for="cat in categories" :key="cat.id" 
            @click="goList(cat.id)">
        <image :src="cat.icon" class="cat-icon"></image>
        <text class="cat-name">{{ cat.name }}</text>
      </view>
    </view>

    <!-- 热门线路 -->
    <view class="section">
      <view class="section-header">
        <text class="section-title">🔥 热门线路</text>
        <text class="section-more" @click="goList">查看更多 ></text>
      </view>

      <view class="route-list">
        <view class="route-card" v-for="route in hotRoutes" :key="route.id"
              @click="goDetail(route.id)">
          <image :src="route.coverImg" class="route-cover" mode="aspectFill"></image>
          <view class="route-info">
            <text class="route-title">{{ route.title }}</text>
            <text class="route-desc">{{ route.destination }} · {{ route.days }}天{{ route.days-1 }}晚</text>
            <view class="route-bottom">
              <text class="route-price">
                <text class="yen">¥</text>{{ route.price }}
                <text class="unit">/人起</text>
              </text>
              <text class="route-sales">{{ route.sales }}人已预订</text>
            </view>
          </view>
        </view>
      </view>
    </view>
  </view>
</template>

<script>
export default {
  data() {
    return {
      banners: [],
      hotRoutes: [],
      categories: [
        { id: 1, name: '海边度假', icon: '/static/icons/beach.png' },
        { id: 2, name: '山水风光', icon: '/static/icons/mountain.png' },
        { id: 3, name: '古城文化', icon: '/static/icons/city.png' },
        { id: 4, name: '亲子游', icon: '/static/icons/family.png' },
        { id: 5, name: '美食之旅', icon: '/static/icons/food.png' },
        { id: 6, name: '更多', icon: '/static/icons/more.png' }
      ]
    }
  },
  onLoad() {
    this.loadHomeData()
  },
  onPullDownRefresh() {
    this.loadHomeData()
    uni.stopPullDownRefresh()
  },
  methods: {
    async loadHomeData() {
      uni.showLoading({ title: '加载中...' })
      try {
        const res = await uni.request({
          url: 'http://localhost:8080/api/travel/home',
          method: 'GET'
        })
        if (res.data.code === 200) {
          this.banners = res.data.data.banners
          this.hotRoutes = res.data.data.hotRoutes
        }
      } catch (e) {
        console.error(e)
      } finally {
        uni.hideLoading()
      }
    },
    goDetail(id) {
      uni.navigateTo({ url: `/pages/detail/detail?id=${id}` })
    },
    goList(catId) {
      uni.switchTab({ url: '/pages/list/list' })
    },
    goSearch() {
      uni.navigateTo({ url: '/pages/list/list?keyword=' })
    }
  }
}
</script>

<style scoped>
.search-bar {
  padding: 20rpx 30rpx;
  background: #fff;
}
.search-input {
  display: flex;
  align-items: center;
  background: #f5f5f5;
  border-radius: 40rpx;
  padding: 16rpx 30rpx;
}
.placeholder {
  margin-left: 16rpx;
  color: #999;
  font-size: 28rpx;
}
.banner {
  height: 360rpx;
  margin: 20rpx 30rpx;
  border-radius: 20rpx;
  overflow: hidden;
}
.banner-img {
  width: 100%;
  height: 100%;
}
.category-grid {
  display: flex;
  flex-wrap: wrap;
  padding: 20rpx 30rpx;
  background: #fff;
  margin: 0 30rpx;
  border-radius: 20rpx;
}
.cat-item {
  width: 20%;
  display: flex;
  flex-direction: column;
  align-items: center;
  margin-bottom: 20rpx;
}
.cat-icon {
  width: 80rpx;
  height: 80rpx;
}
.cat-name {
  font-size: 24rpx;
  color: #333;
  margin-top: 8rpx;
}
.section {
  margin-top: 30rpx;
  padding: 0 30rpx;
}
.section-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 20rpx;
}
.section-title {
  font-size: 34rpx;
  font-weight: bold;
  color: #333;
}
.section-more {
  font-size: 26rpx;
  color: #07c160;
}
.route-card {
  display: flex;
  background: #fff;
  border-radius: 20rpx;
  margin-bottom: 24rpx;
  overflow: hidden;
  box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.06);
}
.route-cover {
  width: 240rpx;
  height: 200rpx;
  flex-shrink: 0;
}
.route-info {
  flex: 1;
  padding: 16rpx 20rpx;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
}
.route-title {
  font-size: 28rpx;
  font-weight: bold;
  color: #333;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}
.route-desc {
  font-size: 24rpx;
  color: #999;
  margin-top: 8rpx;
}
.route-bottom {
  display: flex;
  justify-content: space-between;
  align-items: flex-end;
}
.route-price {
  color: #ff4d4f;
  font-weight: bold;
  font-size: 32rpx;
}
.yen {
  font-size: 24rpx;
}
.unit {
  font-size: 22rpx;
  color: #999;
  font-weight: normal;
}
.route-sales {
  font-size: 22rpx;
  color: #bbb;
}
</style>

4️⃣ pages/detail/detail.vue - 线路详情 ⭐


vue

<template>
  <view class="detail-page">
    <!-- 封面图 -->
    <image :src="route.coverImg" class="cover" mode="aspectFill"></image>

    <!-- 基本信息 -->
    <view class="info-card">
      <text class="title">{{ route.title }}</text>
      <view class="tags">
        <text class="tag">{{ route.destination }}</text>
        <text class="tag">{{ route.days }}天{{ route.days-1 }}晚</text>
        <text class="tag">{{ route.departure }}出发</text>
      </view>
      <view class="price-row">
        <text class="price">
          <text class="yen">¥</text>{{ route.price }}
          <text class="unit">/人起</text>
        </text>
        <text class="sales">{{ route.sales }}人已预订</text>
      </view>
    </view>

    <!-- 行程安排 -->
    <view class="section-card">
      <text class="card-title">📋 行程安排</text>
      <view class="schedule-item" v-for="(item, index) in schedule" :key="index">
        <view class="schedule-day">Day {{ index + 1 }}</view>
        <text class="schedule-content">{{ item }}</text>
      </view>
    </view>

    <!-- 费用说明 -->
    <view class="section-card">
      <text class="card-title">💰 费用说明</text>
      <view class="fee-item" v-for="(item, index) in fees" :key="index">
        <text class="fee-label">{{ item.label }}</text>
        <text class="fee-value" :class="{ included: item.included }">
          {{ item.value }}
        </text>
      </view>
    </view>

    <!-- 底部操作栏 -->
    <view class="bottom-bar">
      <view class="bar-left">
        <view class="bar-item" @click="toggleFavorite">
          <uni-icons :type="isFav ? 'heart-filled' : 'heart'" size="24" 
                     :color="isFav ? '#ff4d4f' : '#999'"></uni-icons>
          <text>收藏</text>
        </view>
        <view class="bar-item" @click="shareRoute">
          <uni-icons type="redo" size="24" color="#999"></uni-icons>
          <text>分享</text>
        </view>
      </view>
      <view class="bar-right">
        <button class="btn-book" @click="goOrder">立即预订</button>
      </view>
    </view>
  </view>
</template>

<script>
export default {
  data() {
    return {
      routeId: null,
      route: {},
      schedule: [],
      fees: [
        { label: '住宿', value: '含', included: true },
        { label: '用餐', value: '含早餐', included: true },
        { label: '交通', value: '含', included: true },
        { label: '门票', value: '含', included: true },
        { label: '导游', value: '含', included: true },
        { label: '保险', value: '含', included: true }
      ],
      isFav: false
    }
  },
  onLoad(options) {
    this.routeId = options.id
    this.loadDetail()
  },
  methods: {
    async loadDetail() {
      uni.showLoading({ title: '加载中...' })
      try {
        const res = await uni.request({
          url: `http://localhost:8080/api/travel/detail/${this.routeId}`
        })
        if (res.data.code === 200) {
          this.route = res.data.data
          // 模拟行程
          this.schedule = [
            `抵达${this.route.destination},专车接站,入住酒店休息`,
            `游览${this.route.destination}核心景区,品尝当地特色美食`,
            `自由活动,可选择自费项目`,
            `返回${this.route.departure},结束愉快旅程`
          ]
        }
      } catch (e) {
        console.error(e)
      } finally {
        uni.hideLoading()
      }
    },
    goOrder() {
      uni.navigateTo({
        url: `/pages/order/order?id=${this.routeId}&price=${this.route.price}`
      })
    },
    toggleFavorite() {
      this.isFav = !this.isFav
      uni.showToast({
        title: this.isFav ? '已收藏' : '已取消',
        icon: 'none'
      })
    },
    shareRoute() {
      // 触发分享
    },
    onShareAppMessage() {
      return {
        title: this.route.title,
        path: `/pages/detail/detail?id=${this.routeId}`
      }
    }
  }
}
</script>

<style scoped>
.detail-page {
  padding-bottom: 140rpx;
}
.cover {
  width: 100%;
  height: 500rpx;
}
.info-card {
  background: #fff;
  margin: -40rpx 30rpx 20rpx;
  border-radius: 20rpx;
  padding: 30rpx;
  position: relative;
  box-shadow: 0 4rpx 20rpx rgba(0,0,0,0.08);
}
.title {
  font-size: 36rpx;
  font-weight: bold;
  color: #333;
}
.tags {
  display: flex;
  gap: 16rpx;
  margin-top: 16rpx;
}
.tag {
  background: #e8f5e9;
  color: #07c160;
  font-size: 24rpx;
  padding: 6rpx 16rpx;
  border-radius: 20rpx;
}
.price-row {
  display: flex;
  justify-content: space-between;
  align-items: flex-end;
  margin-top: 20rpx;
}
.price {
  color: #ff4d4f;
  font-weight: bold;
  font-size: 40rpx;
}
.unit {
  font-size: 24rpx;
  color: #999;
  font-weight: normal;
}
.sales {
  font-size: 24rpx;
  color: #bbb;
}
.section-card {
  background: #fff;
  margin: 20rpx 30rpx;
  border-radius: 20rpx;
  padding: 30rpx;
}
.card-title {
  font-size: 32rpx;
  font-weight: bold;
  color: #333;
  margin-bottom: 20rpx;
  display: block;
}
.schedule-item {
  display: flex;
  margin-bottom: 20rpx;
}
.schedule-day {
  background: #07c160;
  color: #fff;
  font-size: 22rpx;
  padding: 4rpx 16rpx;
  border-radius: 8rpx;
  margin-right: 16rpx;
  height: fit-content;
}
.schedule-content {
  font-size: 28rpx;
  color: #666;
  line-height: 1.6;
}
.fee-item {
  display: flex;
  justify-content: space-between;
  padding: 16rpx 0;
  border-bottom: 1rpx solid #f0f0f0;
}
.fee-label {
  font-size: 28rpx;
  color: #333;
}
.fee-value {
  font-size: 28rpx;
  color: #999;
}
.fee-value.included {
  color: #07c160;
}
.bottom-bar {
  position: fixed;
  bottom: 0;
  left: 0;
  right: 0;
  height: 120rpx;
  background: #fff;
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0 30rpx;
  box-shadow: 0 -4rpx 20rpx rgba(0,0,0,0.06);
  padding-bottom: env(safe-area-inset-bottom);
}
.bar-left {
  display: flex;
  gap: 40rpx;
}
.bar-item {
  display: flex;
  flex-direction: column;
  align-items: center;
  font-size: 22rpx;
  color: #999;
}
.btn-book {
  background: linear-gradient(135deg, #07c160, #06ad56);
  color: #fff;
  border: none;
  border-radius: 50rpx;
  padding: 24rpx 80rpx;
  font-size: 32rpx;
  font-weight: bold;
}
</style>

5️⃣ pages/order/order.vue - 确认订单


vue

<template>
  <view class="order-page">
    <!-- 订单信息 -->
    <view class="order-card">
      <text class="card-title">订单信息</text>
      <view class="order-item">
        <text class="label">线路</text>
        <text class="value">{{ route.title }}</text>
      </view>
      <view class="order-item">
        <text class="label">出发日期</text>
        <picker mode="date" @change="onDateChange">
          <text class="value">{{ travelDate || '请选择' }}</text>
        </picker>
      </view>
      <view class="order-item">
        <text class="label">出行人数</text>
        <view class="counter">
          <view class="counter-btn" @click="changeCount(-1)">-</view>
          <text class="counter-num">{{ count }}</text>
          <view class="counter-btn" @click="changeCount(1)">+</view>
        </view>
      </view>
    </view>

    <!-- 联系人 -->
    <view class="order-card">
      <text class="card-title">联系人信息</text>
      <view class="order-item">
        <text class="label">姓名</text>
        <input class="input" v-model="form.name" placeholder="请输入姓名" />
      </view>
      <view class="order-item">
        <text class="label">手机号</text>
        <input class="input" v-model="form.phone" type="number" 
               placeholder="请输入手机号" maxlength="11" />
      </view>
    </view>

    <!-- 价格明细 -->
    <view class="order-card">
      <text class="card-title">💰 价格明细</text>
      <view class="price-row">
        <text>线路费用</text>
        <text>¥{{ route.price }} × {{ count }}人</text>
      </view>
      <view class="price-row total">
        <text>合计</text>
        <text class="total-price">¥{{ totalPrice }}</text>
      </view>
    </view>

    <!-- 提交按钮 -->
    <view class="submit-bar">
      <text class="total-label">合计:</text>
      <text class="total-amount">¥{{ totalPrice }}</text>
      <button class="submit-btn" @click="submitOrder">提交订单</button>
    </view>
  </view>
</template>

<script>
export default {
  data() {
    return {
      routeId: null,
      route: {},
      count: 1,
      travelDate: '',
      form: {
        name: '',
        phone: ''
      }
    }
  },
  computed: {
    totalPrice() {
      return (this.route.price * this.count).toFixed(2)
    }
  },
  onLoad(options) {
    this.routeId = options.id
    this.route.price = parseFloat(options.price)
    this.loadRoute()
  },
  methods: {
    async loadRoute() {
      const res = await uni.request({
        url: `http://localhost:8080/api/travel/detail/${this.routeId}`
      })
      if (res.data.code === 200) {
        this.route = res.data.data
      }
    },
    changeCount(delta) {
      this.count = Math.max(1, Math.min(10, this.count + delta))
    },
    onDateChange(e) {
      this.travelDate = e.detail.value
    },
    async submitOrder() {
      if (!this.form.name) return uni.showToast({ title: '请输入姓名', icon: 'none' })
      if (!this.form.phone || this.form.phone.length !== 11) {
        return uni.showToast({ title: '请输入正确手机号', icon: 'none' })
      }
      if (!this.travelDate) return uni.showToast({ title: '请选择日期', icon: 'none' })

      uni.showLoading({ title: '提交中...' })
      try {
        const res = await uni.request({
          url: 'http://localhost:8080/api/travel/order',
          method: 'POST',
          data: {
            routeId: this.routeId,
            price: this.route.price,
            count: this.count,
            travelDate: this.travelDate,
            name: this.form.name,
            phone: this.form.phone
          }
        })
        if (res.data.code === 200) {
          uni.showModal({
            title: '下单成功',
            content: `订单号:${res.data.data}`,
            showCancel: false,
            success: () => {
              uni.switchTab({ url: '/pages/mine/mine' })
            }
          })
        }
      } catch (e) {
        console.error(e)
      } finally {
        uni.hideLoading()
      }
    }
  }
}
</script>

<style scoped>
.order-page {
  padding: 20rpx 30rpx;
  padding-bottom: 180rpx;
}
.order-card {
  background: #fff;
  border-radius: 20rpx;
  padding: 30rpx;
  margin-bottom: 20rpx;
}
.card-title {
  font-size: 30rpx;
  font-weight: bold;
  color: #333;
  margin-bottom: 20rpx;
  display: block;
}
.order-item {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 16rpx 0;
  border-bottom: 1rpx solid #f5f5f5;
}
.label {
  font-size: 28rpx;
  color: #666;
}
.value {
  font-size: 28rpx;
  color: #333;
}
.input {
  text-align: right;
  width: 400rpx;
  font-size: 28rpx;
}
.counter {
  display: flex;
  align-items: center;
  gap: 24rpx;
}
.counter-btn {
  width: 56rpx;
  height: 56rpx;
  border-radius: 50%;
  background: #f5f5f5;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 32rpx;
  color: #333;
}
.counter-num {
  font-size: 32rpx;
  font-weight: bold;
  min-width: 60rpx;
  text-align: center;
}
.price-row {
  display: flex;
  justify-content: space-between;
  padding: 12rpx 0;
  font-size: 28rpx;
  color: #666;
}
.price-row.total {
  border-top: 1rpx solid #f0f0f0;
  margin-top: 10rpx;
  padding-top: 20rpx;
  font-weight: bold;
  color: #333;
}
.total-price {
  color: #ff4d4f;
  font-size: 36rpx;
}
.submit-bar {
  position: fixed;
  bottom: 0;
  left: 0;
  right: 0;
  height: 120rpx;
  background: #fff;
  display: flex;
  align-items: center;
  padding: 0 30rpx;
  box-shadow: 0 -4rpx 20rpx rgba(0,0,0,0.06);
  padding-bottom: env(safe-area-inset-bottom);
}
.total-label {
  font-size: 28rpx;
  color: #666;
}
.total-amount {
  font-size: 36rpx;
  color: #ff4d4f;
  font-weight: bold;
  margin-right: 20rpx;
}
.submit-btn {
  background: linear-gradient(135deg, #07c160, #06ad56);
  color: #fff;
  border: none;
  border-radius: 50rpx;
  padding: 20rpx 60rpx;
  font-size: 30rpx;
  font-weight: bold;
}
</style>

🗄️ 三、数据库SQL


sql

CREATE DATABASE travel_system DEFAULT CHARSET utf8mb4;

USE travel_system;

-- 旅游线路表
CREATE TABLE travel_route (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    title VARCHAR(200) NOT NULL COMMENT '线路标题',
    cover_img VARCHAR(500) COMMENT '封面图',
    description TEXT COMMENT '描述',
    price DECIMAL(10,2) COMMENT '价格',
    days INT COMMENT '天数',
    departure VARCHAR(100) COMMENT '出发地',
    destination VARCHAR(100) COMMENT '目的地',
    status TINYINT DEFAULT 1 COMMENT '0下架 1上架',
    sales INT DEFAULT 0 COMMENT '销量',
    create_time DATETIME DEFAULT CURRENT_TIMESTAMP
) COMMENT '旅游线路表';

-- 订单表
CREATE TABLE `order` (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    order_no VARCHAR(64) UNIQUE NOT NULL COMMENT '订单号',
    user_id BIGINT NOT NULL COMMENT '用户ID',
    route_id BIGINT NOT NULL COMMENT '线路ID',
    price DECIMAL(10,2) COMMENT '总价',
    count INT DEFAULT 1 COMMENT '人数',
    travel_date DATE COMMENT '出行日期',
    name VARCHAR(50) COMMENT '联系人',
    phone VARCHAR(20) COMMENT '手机号',
    status TINYINT DEFAULT 0 COMMENT '0待支付 1已支付 2已完成 3已取消',
    create_time DATETIME DEFAULT CURRENT_TIMESTAMP
) COMMENT '订单表';

-- 用户表
CREATE TABLE user (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    openid VARCHAR(100) UNIQUE COMMENT '微信openid',
    nickname VARCHAR(100) COMMENT '昵称',
    avatar VARCHAR(500) COMMENT '头像',
    phone VARCHAR(20) COMMENT '手机号',
    create_time DATETIME DEFAULT CURRENT_TIMESTAMP
) COMMENT '用户表';

-- 插入测试数据
INSERT INTO travel_route (title, cover_img, description, price, days, departure, destination, sales) VALUES
('三亚5天4晚自由行', '/static/images/sanya.jpg', '阳光沙滩,椰风海韵,享受三亚的碧海蓝天', 2999.00, 5, '全国各地', '海南三亚', 3256),
('丽江古城3日游', '/static/images/lijiang.jpg', '漫步古城,感受纳西族文化,远眺玉龙雪山', 1599.00, 3, '昆明', '云南丽江', 2180),
('张家界5天深度游', '/static/images/zhangjiajie.jpg', '阿凡达取景地,玻璃栈道,天门山', 3299.00, 5, '长沙', '湖南张家界', 1890),
('西安4天历史文化游', '/static/images/xian.jpg', '兵马俑、华清池、大雁塔,梦回大唐', 2199.00, 4, '全国各地', '陕西西安', 4521);

🚀 四、快速启动


bash

# 1. 后端启动
cd travel-backend
mvn spring-boot:run

# 2. 前端启动 (HBuilderX 打开 UniApp 项目)
# 或命令行:
cd travel-uniapp
npm run dev:mp-weixin    # 微信小程序
npm run dev:h5           # H5

📊 系统功能总览

模块 功能 状态
🏠 首页 轮播图 + 分类 + 热门线路
📋 线路列表 搜索 + 筛选 + 分页
📝 线路详情 行程 + 费用 + 收藏
🛒 下单 选日期 + 人数 + 联系人
👤 我的 订单列表 + 个人信息
💰 支付 微信支付(需配置) 🔲

💡 提示:微信小程序支付需要在微信商户平台配置,并在后端接入微信支付SDK。如需要支付完整代码,请告诉我!

更多推荐