基于Vue3与Spring Boot的展会信息平台全栈开发实战

Vue 3Spring Boot全栈开发
于 2026-08-02 04:15:00 修改
·本内容遵循CC 4.0 BY-SA版权协议

最近在参与一个展会相关的项目时,遇到了一个典型问题:如何高效地管理、展示和分发展会信息,并让用户能便捷地“看”到他们感兴趣的内容。传统的静态网页或简单的信息列表,在信息爆炸的今天,用户体验和运营效率都大打折扣。本文将围绕构建一个现代化的、数据驱动的展会信息平台展开,从前端展示到后端数据管理,提供一个完整的全栈实战方案。无论你是想学习如何整合前后端技术栈,还是需要为你的业务快速搭建一个类似的信息门户,这篇文章都能提供从零到一的详细指导。

1. 背景与核心概念:什么是现代化的展会信息平台?

在数字化时代,“看展会”早已不是简单地浏览一个网页列表。一个优秀的展会信息平台,应该解决以下几个核心问题:

  1. 信息结构化与可视化:将零散的展会信息(如时间、地点、参展商、展品、活动日程)进行结构化存储,并通过清晰的UI(如日历视图、地图、分类筛选)呈现给用户。
  2. 个性化推荐与搜索:帮助用户从海量信息中快速找到“值得看”的内容。这需要强大的搜索引擎和基于用户行为的推荐算法。
  3. 交互与参与感:提供收藏、预约、在线咨询、直播观看、互动评论等功能,提升用户的参与度和粘性。
  4. 后台高效管理:为运营人员提供便捷的内容管理后台(CMS),能够快速上架、修改展会信息,并查看数据报表。

从技术角度看,这样一个平台通常采用前后端分离的架构。前端负责展示和交互,后端提供数据接口和业务逻辑,数据库则持久化存储所有信息。本文将使用 Vue 3 作为前端框架,Spring Boot 作为后端框架,MySQL 作为数据库,来演示如何一步步构建这个系统。

2. 环境准备与版本说明

在开始编码之前,请确保你的开发环境已就绪。以下是本文示例所使用的主要技术栈及版本,你可以根据实际情况进行调整。

后端环境:

  • JDK: 17 或更高版本 (推荐 Amazon Corretto 17)
  • 构建工具: Maven 3.8+
  • IDE: IntelliJ IDEA 或 Eclipse
  • 数据库: MySQL 8.0+
  • 框架: Spring Boot 3.1.x

前端环境:

  • Node.js: 18.x 或更高版本 (推荐 LTS 版本)
  • 包管理器: npm 或 yarn
  • IDE: Visual Studio Code 或 WebStorm
  • 框架: Vue 3 + TypeScript + Vite
  • UI 库: Element Plus (基于 Vue 3)

项目结构预览: 我们将创建两个独立的项目:

TEXT
exhibition-platform/
├── backend/ # Spring Boot 后端项目
└── frontend/ # Vue 3 前端项目

请先确保 MySQL 服务已启动,并创建一个名为 exhibition_db 的数据库。

SQL
CREATE DATABASE IF NOT EXISTS exhibition_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

3. 核心模块设计与数据模型拆解

在动手写代码前,合理的数据库设计和API规划至关重要。我们围绕“展会”核心实体进行扩展。

3.1 数据库表设计

主要包含以下几个表:

  1. 展会主表 (exhibition):存储展会基本信息。
  2. 参展商表 (exhibitor):存储参展商信息,与展会多对多关联。
  3. 展品表 (exhibit):存储展品信息,归属于某个参展商。
  4. 活动日程表 (schedule):存储展会期间的具体活动。
  5. 用户表 (user):存储注册用户信息,用于收藏、预约等行为。
  6. 关联表:如 exhibition_exhibitor (展会-参展商关联)、user_favorite (用户收藏)。

以下是核心表 exhibition 的建表语句示例:

SQL
-- 展会主表
CREATE TABLE `exhibition` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`title` varchar(200) NOT NULL COMMENT '展会标题',
`description` text COMMENT '展会详细描述',
`cover_image_url` varchar(500) COMMENT '封面图URL',
`start_time` datetime NOT NULL COMMENT '开始时间',
`end_time` datetime NOT NULL COMMENT '结束时间',
`venue` varchar(200) NOT NULL COMMENT '举办地点',
`venue_coordinate` point COMMENT '地点坐标(用于地图)',
`status` tinyint NOT NULL DEFAULT '1' COMMENT '状态:1-筹备中,2-进行中,3-已结束',
`is_recommended` tinyint NOT NULL DEFAULT '0' COMMENT '是否推荐:0-否,1-是',
`view_count` int NOT NULL DEFAULT '0' COMMENT '浏览量',
`created_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `idx_status_time` (`status`, `start_time`),
KEY `idx_recommended` (`is_recommended`),
SPATIAL KEY `idx_coordinate` (`venue_coordinate`) -- 空间索引,用于地理位置查询
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='展会信息表';

3.2 后端API设计 (RESTful风格)

我们将为展会资源设计一组标准的 REST API:

  • GET /api/exhibitions:获取展会列表(支持分页、筛选、排序)
  • GET /api/exhibitions/{id}:获取单个展会详情
  • GET /api/exhibitions/recommended:获取推荐展会列表
  • POST /api/exhibitions:创建新展会 (管理员)
  • PUT /api/exhibitions/{id}:更新展会信息 (管理员)
  • POST /api/exhibitions/{id}/view:增加展会浏览量
  • GET /api/exhibitions/search:搜索展会 (全文/条件搜索)

其他实体(参展商、展品等)的API设计模式类似。

4. 完整实战案例:构建展会列表与详情页

接下来,我们从后端到前端,实现最核心的“看展会”功能:展示展会列表和查看展会详情。

4.1 后端实现 (Spring Boot)

步骤1:创建Spring Boot项目并添加依赖 使用 Spring Initializr 或IDE创建项目,选择依赖:Spring Web, Spring Data JPA, MySQL Driver, Lombok

pom.xml 关键依赖片段:

XML
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- 可选,用于参数校验 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
</dependencies>

步骤2:配置数据库连接 application.yml 配置文件:

YAML
spring:
datasource:
url: jdbc:mysql://localhost:3306/exhibition_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
username: root
password: your_password
driver-class-name: com.mysql.cj.jdbc.Driver
jpa:
hibernate:
ddl-auto: update # 开发环境可用update,生产环境建议使用none或validate,并通过SQL脚本管理
show-sql: true
properties:
hibernate:
dialect: org.hibernate.dialect.MySQL8Dialect
format_sql: true
 
server:
port: 8080

步骤3:创建实体类(Entity) 对应 exhibition 表,创建 Exhibition 实体类。

JAVA
// 文件路径:src/main/java/com/example/exhibition/entity/Exhibition.java
package com.example.exhibition.entity;
 
import jakarta.persistence.*;
import lombok.Data;
import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.UpdateTimestamp;
import java.time.LocalDateTime;
 
@Entity
@Table(name = "exhibition")
@Data
public class Exhibition {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
 
@Column(nullable = false, length = 200)
private String title;
 
@Column(columnDefinition = "TEXT")
private String description;
 
@Column(name = "cover_image_url", length = 500)
private String coverImageUrl;
 
@Column(name = "start_time", nullable = false)
private LocalDateTime startTime;
 
@Column(name = "end_time", nullable = false)
private LocalDateTime endTime;
 
@Column(nullable = false, length = 200)
private String venue;
 
// 状态枚举:1-筹备中,2-进行中,3-已结束
@Column(nullable = false)
private Integer status = 1;
 
@Column(name = "is_recommended", nullable = false)
private Boolean isRecommended = false;
 
@Column(name = "view_count", nullable = false)
private Integer viewCount = 0;
 
@CreationTimestamp
@Column(name = "created_time", updatable = false)
private LocalDateTime createdTime;
 
@UpdateTimestamp
@Column(name = "updated_time")
private LocalDateTime updatedTime;
}

步骤4:创建数据访问层(Repository) 利用 Spring Data JPA 简化数据库操作。

JAVA
// 文件路径:src/main/java/com/example/exhibition/repository/ExhibitionRepository.java
package com.example.exhibition.repository;
 
import com.example.exhibition.entity.Exhibition;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.stereotype.Repository;
import java.time.LocalDateTime;
import java.util.List;
 
@Repository
public interface ExhibitionRepository extends JpaRepository<Exhibition, Long> {
 
// 查找推荐展会
List<Exhibition> findByIsRecommendedTrueOrderByStartTimeDesc();
 
// 根据状态分页查询
Page<Exhibition> findByStatus(Integer status, Pageable pageable);
 
// 复杂查询示例:查找正在进行或即将开始的展会
@Query("SELECT e FROM Exhibition e WHERE e.status = 2 OR (e.status = 1 AND e.startTime <= :threshold)")
List<Exhibition> findActiveOrUpcoming(LocalDateTime threshold);
 
// 根据标题关键词搜索 (简单模糊查询)
Page<Exhibition> findByTitleContainingIgnoreCase(String keyword, Pageable pageable);
}

步骤5:创建服务层(Service) 封装业务逻辑。

JAVA
// 文件路径:src/main/java/com/example/exhibition/service/ExhibitionService.java
package com.example.exhibition.service;
 
import com.example.exhibition.entity.Exhibition;
import com.example.exhibition.repository.ExhibitionRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
 
@Service
@RequiredArgsConstructor
public class ExhibitionService {
 
private final ExhibitionRepository exhibitionRepository;
 
public Page<Exhibition> getExhibitionsByPage(Pageable pageable) {
return exhibitionRepository.findAll(pageable);
}
 
public Exhibition getExhibitionById(Long id) {
return exhibitionRepository.findById(id)
.orElseThrow(() -> new RuntimeException("展会不存在,ID: " + id));
}
 
public List<Exhibition> getRecommendedExhibitions() {
return exhibitionRepository.findByIsRecommendedTrueOrderByStartTimeDesc();
}
 
public Page<Exhibition> searchExhibitions(String keyword, Pageable pageable) {
return exhibitionRepository.findByTitleContainingIgnoreCase(keyword, pageable);
}
 
@Transactional
public void incrementViewCount(Long id) {
Exhibition exhibition = getExhibitionById(id);
exhibition.setViewCount(exhibition.getViewCount() + 1);
exhibitionRepository.save(exhibition); // 此处save会触发@UpdateTimestamp
}
 
// 保存或更新展会 (管理员用)
@Transactional
public Exhibition saveOrUpdateExhibition(Exhibition exhibition) {
return exhibitionRepository.save(exhibition);
}
}

步骤6:创建控制层(Controller) 暴露 REST API。

JAVA
// 文件路径:src/main/java/com/example/exhibition/controller/ExhibitionController.java
package com.example.exhibition.controller;
 
import com.example.exhibition.entity.Exhibition;
import com.example.exhibition.service.ExhibitionService;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
 
@RestController
@RequestMapping("/api/exhibitions")
@RequiredArgsConstructor
public class ExhibitionController {
 
private final ExhibitionService exhibitionService;
 
@GetMapping
public ResponseEntity<Page<Exhibition>> getExhibitions(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "10") int size,
@RequestParam(defaultValue = "startTime,desc") String sort) {
// 解析排序参数,例如 "startTime,desc"
String[] sortParams = sort.split(",");
Sort.Direction direction = sortParams.length > 1 && "asc".equalsIgnoreCase(sortParams[1]) ? Sort.Direction.ASC : Sort.Direction.DESC;
Pageable pageable = PageRequest.of(page, size, Sort.by(direction, sortParams[0]));
Page<Exhibition> exhibitions = exhibitionService.getExhibitionsByPage(pageable);
return ResponseEntity.ok(exhibitions);
}
 
@GetMapping("/{id}")
public ResponseEntity<Exhibition> getExhibitionById(@PathVariable Long id) {
Exhibition exhibition = exhibitionService.getExhibitionById(id);
// 可以在这里异步增加浏览量,避免阻塞主请求
// exhibitionService.incrementViewCount(id);
return ResponseEntity.ok(exhibition);
}
 
@GetMapping("/recommended")
public ResponseEntity<List<Exhibition>> getRecommendedExhibitions() {
List<Exhibition> exhibitions = exhibitionService.getRecommendedExhibitions();
return ResponseEntity.ok(exhibitions);
}
 
@GetMapping("/search")
public ResponseEntity<Page<Exhibition>> searchExhibitions(
@RequestParam String keyword,
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "10") int size) {
Pageable pageable = PageRequest.of(page, size);
Page<Exhibition> result = exhibitionService.searchExhibitions(keyword, pageable);
return ResponseEntity.ok(result);
}
 
@PostMapping("/{id}/view")
public ResponseEntity<Void> incrementViewCount(@PathVariable Long id) {
exhibitionService.incrementViewCount(id);
return ResponseEntity.ok().build();
}
 
// 管理员接口示例,应添加权限校验(如使用Spring Security)
@PostMapping
public ResponseEntity<Exhibition> createExhibition(@RequestBody Exhibition exhibition) {
Exhibition saved = exhibitionService.saveOrUpdateExhibition(exhibition);
return ResponseEntity.ok(saved);
}
}

启动 Spring Boot 应用 (ExhibitionApplication),访问 http://localhost:8080/api/exhibitions 即可测试API。

4.2 前端实现 (Vue 3 + Element Plus)

步骤1:创建Vue项目并安装依赖

BASH
# 在 frontend 目录下执行
npm create vue@latest exhibition-frontend
# 根据提示选择:TypeScript, Router, Pinia, ESLint
cd exhibition-frontend
npm install
# 安装 Element Plus 和 Axios
npm install element-plus @element-plus/icons-vue axios

步骤2:配置 Axios 和 API 基础路径 创建 src/utils/request.ts 文件:

TYPESCRIPT
// 文件路径:src/utils/request.ts
import axios from 'axios';
 
const request = axios.create({
baseURL: 'http://localhost:8080/api', // 后端API地址
timeout: 10000,
});
 
// 请求拦截器
request.interceptors.request.use(
(config) => {
// 可以在这里统一添加token等
// const token = localStorage.getItem('token');
// if (token) {
// config.headers.Authorization = `Bearer ${token}`;
// }
return config;
},
(error) => {
return Promise.reject(error);
}
);
 
// 响应拦截器
request.interceptors.response.use(
(response) => {
return response.data;
},
(error) => {
// 统一错误处理
console.error('API请求错误:', error);
ElMessage.error(error.response?.data?.message || '网络请求失败');
return Promise.reject(error);
}
);
 
export default request;

步骤3:创建展会服务模块 创建 src/services/exhibition.ts 文件:

TYPESCRIPT
// 文件路径:src/services/exhibition.ts
import request from '@/utils/request';
import type { Exhibition } from '@/types/exhibition'; // 需要定义类型
 
// 定义分页响应类型
export interface PageResponse<T> {
content: T[];
totalElements: number;
totalPages: number;
size: number;
number: number;
}
 
export const exhibitionApi = {
// 获取展会列表
getExhibitions(page: number = 0, size: number = 10, sort: string = 'startTime,desc') {
return request.get<PageResponse<Exhibition>>('/exhibitions', {
params: { page, size, sort }
});
},
 
// 获取展会详情
getExhibitionById(id: number) {
return request.get<Exhibition>(`/exhibitions/${id}`);
},
 
// 获取推荐展会
getRecommendedExhibitions() {
return request.get<Exhibition[]>('/exhibitions/recommended');
},
 
// 搜索展会
searchExhibitions(keyword: string, page: number = 0, size: number = 10) {
return request.get<PageResponse<Exhibition>>('/exhibitions/search', {
params: { keyword, page, size }
});
},
 
// 增加浏览量
incrementViewCount(id: number) {
return request.post(`/exhibitions/${id}/view`);
}
};

步骤4:定义 TypeScript 类型 创建 src/types/exhibition.ts 文件:

TYPESCRIPT
// 文件路径:src/types/exhibition.ts
export interface Exhibition {
id: number;
title: string;
description?: string;
coverImageUrl?: string;
startTime: string; // 后端返回的是字符串格式时间
endTime: string;
venue: string;
status: number;
isRecommended: boolean;
viewCount: number;
createdTime: string;
updatedTime: string;
}

步骤5:创建展会列表组件 创建 src/views/ExhibitionListView.vue 文件:

VUE
<!-- 文件路径:src/views/ExhibitionListView.vue -->
<template>
<div class="exhibition-list">
<div class="header">
<h1>展会列表</h1>
<el-input
v-model="searchKeyword"
placeholder="搜索展会..."
style="width: 300px; margin-left: 20px;"
@keyup.enter="handleSearch"
>
<template #append>
<el-button :icon="Search" @click="handleSearch" />
</template>
</el-input>
</div>
 
<!-- 推荐展会 -->
<div v-if="recommendedExhibitions.length > 0" class="recommended-section">
<h2>🌟 推荐展会</h2>
<el-row :gutter="20">
<el-col
v-for="item in recommendedExhibitions"
:key="item.id"
:xs="24"
:sm="12"
:md="8"
:lg="6"
>
<ExhibitionCard :exhibition="item" />
</el-col>
</el-row>
</div>
 
<!-- 全部展会 -->
<div class="all-exhibitions">
<h2>全部展会</h2>
<el-row :gutter="20">
<el-col
v-for="item in exhibitionList"
:key="item.id"
:xs="24"
:sm="12"
:md="8"
:lg="6"
>
<ExhibitionCard :exhibition="item" />
</el-col>
</el-row>
 
<!-- 分页 -->
<div class="pagination">
<el-pagination
v-model:current-page="currentPage"
v-model:page-size="pageSize"
:page-sizes="[8, 16, 24, 32]"
layout="total, sizes, prev, pager, next, jumper"
:total="total"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
</div>
</div>
</template>
 
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { Search } from '@element-plus/icons-vue';
import ExhibitionCard from '@/components/ExhibitionCard.vue';
import { exhibitionApi } from '@/services/exhibition';
import type { Exhibition } from '@/types/exhibition';
 
const searchKeyword = ref('');
const recommendedExhibitions = ref<Exhibition[]>([]);
const exhibitionList = ref<Exhibition[]>([]);
const currentPage = ref(1);
const pageSize = ref(8);
const total = ref(0);
 
// 加载推荐展会
const loadRecommended = async () => {
try {
const data = await exhibitionApi.getRecommendedExhibitions();
recommendedExhibitions.value = data;
} catch (error) {
console.error('加载推荐展会失败:', error);
}
};
 
// 加载展会列表
const loadExhibitions = async (page = currentPage.value - 1) => {
try {
const data = await exhibitionApi.getExhibitions(page, pageSize.value);
exhibitionList.value = data.content;
total.value = data.totalElements;
} catch (error) {
console.error('加载展会列表失败:', error);
}
};
 
// 搜索展会
const handleSearch = async () => {
if (!searchKeyword.value.trim()) {
loadExhibitions();
return;
}
try {
const data = await exhibitionApi.searchExhibitions(searchKeyword.value, 0, pageSize.value);
exhibitionList.value = data.content;
total.value = data.totalElements;
currentPage.value = 1;
} catch (error) {
console.error('搜索失败:', error);
}
};
 
// 分页事件
const handleSizeChange = (val: number) => {
pageSize.value = val;
loadExhibitions();
};
const handleCurrentChange = (val: number) => {
currentPage.value = val;
loadExhibitions(val - 1);
};
 
onMounted(() => {
loadRecommended();
loadExhibitions();
});
</script>
 
<style scoped>
.exhibition-list {
padding: 20px;
}
.header {
display: flex;
align-items: center;
margin-bottom: 30px;
}
.recommended-section,
.all-exhibitions {
margin-bottom: 40px;
}
.pagination {
margin-top: 30px;
display: flex;
justify-content: center;
}
</style>

步骤6:创建展会卡片组件 创建 src/components/ExhibitionCard.vue 文件:

VUE
<!-- 文件路径:src/components/ExhibitionCard.vue -->
<template>
<el-card class="exhibition-card" shadow="hover" @click="goToDetail">
<template #header>
<div class="card-header">
<span class="title">{{ exhibition.title }}</span>
<el-tag v-if="exhibition.isRecommended" type="warning" size="small">推荐</el-tag>
</div>
</template>
<div class="card-body">
<el-image
v-if="exhibition.coverImageUrl"
:src="exhibition.coverImageUrl"
fit="cover"
class="cover-image"
:preview-src-list="[exhibition.coverImageUrl]"
>
<template #error>
<div class="image-error">暂无图片</div>
</template>
</el-image>
<div v-else class="cover-placeholder">暂无封面</div>
<div class="info">
<div class="info-item">
<el-icon><Calendar /></el-icon>
<span>{{ formatDate(exhibition.startTime) }} - {{ formatDate(exhibition.endTime) }}</span>
</div>
<div class="info-item">
<el-icon><Location /></el-icon>
<span>{{ exhibition.venue }}</span>
</div>
<div class="info-item">
<el-icon><View /></el-icon>
<span>浏览: {{ exhibition.viewCount }}</span>
</div>
</div>
</div>
</el-card>
</template>
 
<script setup lang="ts">
import { Calendar, Location, View } from '@element-plus/icons-vue';
import { useRouter } from 'vue-router';
import type { Exhibition } from '@/types/exhibition';
 
const props = defineProps<{
exhibition: Exhibition;
}>();
 
const router = useRouter();
 
const formatDate = (dateStr: string) => {
return new Date(dateStr).toLocaleDateString('zh-CN');
};
 
const goToDetail = () => {
router.push(`/exhibition/${props.exhibition.id}`);
};
</script>
 
<style scoped>
.exhibition-card {
margin-bottom: 20px;
cursor: pointer;
transition: transform 0.2s;
}
.exhibition-card:hover {
transform: translateY(-5px);
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.title {
font-weight: bold;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.cover-image,
.cover-placeholder {
width: 100%;
height: 180px;
border-radius: 4px;
background-color: #f5f7fa;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 12px;
}
.image-error,
.cover-placeholder {
color: #909399;
}
.info {
font-size: 13px;
color: #606266;
}
.info-item {
display: flex;
align-items: center;
margin-bottom: 6px;
}
.info-item .el-icon {
margin-right: 6px;
}
</style>

步骤7:配置路由并运行src/router/index.ts 中添加路由:

TYPESCRIPT
// 文件路径:src/router/index.ts
import { createRouter, createWebHistory } from 'vue-router';
import ExhibitionListView from '@/views/ExhibitionListView.vue';
import ExhibitionDetailView from '@/views/ExhibitionDetailView.vue'; // 需创建详情页组件
 
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: '/',
name: 'home',
component: ExhibitionListView,
},
{
path: '/exhibition/:id',
name: 'exhibition-detail',
component: ExhibitionDetailView,
props: true,
},
// ... 其他路由
],
});
 
export default router;

运行前端项目:

BASH
npm run dev

访问 http://localhost:5173 即可看到展会列表页面。点击卡片可以跳转到详情页(需自行实现 ExhibitionDetailView.vue)。

5. 常见问题与排查思路

在开发过程中,你可能会遇到以下典型问题:

问题现象 常见原因 解决思路
前端访问后端API出现CORS错误 浏览器同源策略阻止 在后端添加CORS配置。在Spring Boot的配置类或Controller上添加@CrossOrigin注解,或使用WebMvcConfigurer全局配置。
数据库连接失败 1. MySQL服务未启动
2. 连接URL、用户名、密码错误
3. 时区设置问题
1. 检查MySQL服务状态。
2. 核对application.yml中的配置。
3. 在连接URL中添加&serverTimezone=Asia/Shanghai
JPA实体类字段与数据库列名映射错误 1. 命名风格不一致(驼峰vs下划线)
2. @Column注解未指定name属性
1. 检查spring.jpa.hibernate.naming.physical-strategy配置。
2. 在实体类字段上显式使用@Column(name = “xxx”)
前端页面空白,控制台报错 1. 组件引入路径错误
2. TypeScript类型错误
3. API返回数据结构不符
1. 检查浏览器控制台(Console)报错信息。
2. 检查Vue组件导入路径和名称。
3. 使用开发者工具Network标签查看API响应数据。
分页查询返回数据不对 1. 前端page参数从1开始,后端从0开始
2. 排序参数格式错误
1. 前后端统一分页参数起始索引(通常后端Pageable从0开始)。
2. 确保排序参数字符串格式与后端解析逻辑一致。
图片无法加载 1. 图片URL路径错误
2. 图片服务器跨域或权限问题
3. 使用本地路径未正确配置静态资源
1. 检查数据库中的URL是否正确。
2. 对于本地图片,可放入public目录或通过后端提供静态资源服务。

6. 最佳实践与工程建议

将基础功能跑通只是第一步,要让项目具备生产可用性,还需要关注以下方面:

1. 后端工程化

  • 统一响应封装:创建Result<T>类,统一API返回格式(code, message, data)。
  • 全局异常处理:使用@ControllerAdvice@ExceptionHandler捕获并处理各类异常,返回友好的错误信息。
  • 参数校验:在DTO类中使用@Validated@NotBlank@Size等注解进行入参校验。
  • 日志记录:使用SLF4J + Logback,合理设置日志级别,记录关键业务操作和异常堆栈。
  • API文档:集成Spring Doc OpenAPI (Swagger),自动生成并维护API文档。
  • 配置分离:使用application-dev.yml, application-prod.yml区分环境配置。

2. 前端工程化

  • 状态管理:对于复杂的跨组件状态(如用户登录态),使用Pinia进行集中管理。
  • 路由守卫:在路由配置中添加导航守卫,用于权限校验和页面访问控制。
  • 组件封装:将可复用的UI逻辑(如时间格式化、图片预览)抽离为自定义指令或Composable函数。
  • 环境变量:使用.env.development.env.production管理不同环境的后端API地址。
  • 性能优化:对图片使用懒加载,对列表使用虚拟滚动,对API请求合理使用防抖节流。

3. 安全与权限

  • 认证与授权:集成Spring Security + JWT,实现用户登录、Token签发与校验、接口权限控制。
  • SQL注入防护:坚持使用JPA或MyBatis的参数化查询,严禁在代码中拼接SQL字符串。
  • XSS防护:前端对用户输入进行转义,后端在存储和输出时也可考虑进行过滤。
  • 敏感信息脱敏:用户手机号、邮箱等敏感信息在接口返回和日志中应进行脱敏处理。

4. 性能与扩展

  • 数据库索引:为高频查询条件(如status, start_time, is_recommended)建立合适索引。
  • 缓存策略:对不常变的展会详情、推荐列表等数据,使用Redis进行缓存,减轻数据库压力。
  • 图片处理与CDN:用户上传的封面图应进行压缩,并存储到对象存储(如OSS、MinIO),通过CDN加速访问。
  • 搜索优化:简单的LIKE查询性能低下,对于复杂的搜索需求,应集成Elasticsearch或使用数据库全文索引。
  • 异步处理:增加浏览量、发送通知等非核心操作,可以放入消息队列(如RabbitMQ)异步处理,提升接口响应速度。

5. 部署与监控

  • 容器化:使用Docker将前后端应用分别容器化,通过Docker Compose编排,简化部署。
  • CI/CD:搭建GitLab CI或Jenkins流水线,实现代码提交后自动构建、测试和部署。
  • 健康检查:后端暴露/actuator/health端点,便于运维监控应用状态。
  • 日志收集:使用ELK(Elasticsearch, Logstash, Kibana)或类似方案集中收集和查看日志。

从“这展会好啊,这展会得看啊”这样一个需求出发,我们完成了一个具备基本功能的展会信息平台。它包含了清晰的数据模型、稳健的后端API、以及一个交互友好的前端界面。在实际项目中,你可以在此基础上继续深化,例如添加用户系统、预约功能、后台管理界面、数据统计分析等模块。技术选型没有绝对的好坏,关键是理解每项技术解决的问题,并能在业务需求、开发效率和系统稳定性之间找到平衡点。希望这个实战案例能为你提供一个清晰的起点和可扩展的架构思路。

Spring Boot + Vue: 全栈开发实战指南
本文围绕Spring BootVue.js展开全栈开发实战。介绍项目背景技术选型,涵盖Spring Boot后端搭建、数据库集成、业务逻辑控制层开发Vue前端初始化、路由状态管理。还提及前后端交互的跨域问题解决,以及项目部署、优化和测试等重要环节。
llTvTll
2399
AIGC时代:如何快速搞定Spring Boot+Vue全栈开发
随着AIGC技术发展,掌握Spring BootVue.js全栈开发技能很重要。本文介绍Spring BootVue.js基础,阐述两者集成步骤,还给出性能优化最佳实践建议。此外,推荐《快速搞定Spring Boot+Vue全栈开发》,介绍其内容、特点和适合读者。
程序边界
13733
【粉丝福利社】快速搞定Spring Boot+Vue全栈开发
本文介绍了《快速搞定Spring Boot+Vue全栈开发》一书。该书剖析Spring Boot 3Vue 3结合的Web开发方案,涵盖RESTful API等核心技术。全书共8章,通过实战演练讲解关键技术,适合不同水平开发者,助其掌握前后端集成应用。
愚公搬代码
161491
Spring Boot + Vue 全栈开发实战指南
本文深入探讨Spring BootVue.js全栈开发。介绍项目背景技术选型,涵盖Spring Boot后端和Vue前端项目搭建、开发,包括数据库集成、业务逻辑处理、路由配置等。还提及前后端交互跨域问题解决,以及项目部署、优化和测试等重要环节,为开发高效Web应用提供方案。
m0_74823658
1374
新手入门:Spring Boot 3 + Vue 3 全栈开发实战
本教程为新手提供Spring Boot 3 + Vue 3全栈开发实战指导。涵盖环境准备,包括开发工具和版本要求;详细介绍后端Spring Boot 3和前端Vue 3的项目创建、配置及示例;讲解前后端联调、部署指南,还列出常见问题及解决办法,助你掌握全栈开发
老北京儿码农
1498
Spring Boot + Vue全栈开发实战
本文通过实战讲解Spring BootVue.js全栈开发。介绍了Spring Boot自动配置、核心依赖模块,对比传统Spring框架优势及对微服务支持;阐述Vue.js组件化开发前端技术整合及进阶技巧;还涉及前后端分离架构、数据操作、安全增强、项目打包部署和数据库操作优化等内容。
随红
1244
Spring Boot与Vue.js全栈开发项目实战
本文介绍了基于Spring BootVue.js的全栈Web应用开发流程,涵盖前后端分离架构的核心实现技术。前端部分讲解了Vue.js的响应式数据绑定、组件化开发Vue CLI构建工具的使用;后端部分详细说明了Spring Boot项目结构、RESTful API设计实现、嵌入式服务器配置。文章还深入探讨了前后端通信机制、Axios封装、Vue Router路由管理、Spring Boot集成Vue静态资源的方式,并提供了项目部署、性能优化、安全加固及持续集成的完整方案,适合初学者系统掌握现代Web开发技能。
Mr.Poker
1138
Vue3Spring Boot:一个全栈开发者的实战之路
本文介绍了一名全栈开发者在Vue3Spring Boot项目中的实践经验,涵盖前端组件设计、微服务通信、Resilience4j断路器应用及数据库索引优化等内容,展示了现代全栈技术栈的关键技能。
叫我阿柒啊
919
Spring Boot + MyBatis + Vue.js + Element UI 全栈开发实战
本文讲述了在软件开发趋势下,全栈开发的重要性日益凸显。通过一个全栈项目实战,详细讲解如何使用Spring Boot、MyBatis、Vue.js和Element UI开发云笔记应用,涵盖项目需求分析、前期准备、核心功能、业务功能、后台管理、项目升级等内容,旨在帮助开发者提升全栈能力,包括独立分析、设计、框架应用等技能。
图苑
1347
Spring Boot + Vue3 完整开发全栈项目附资料.zip
本项目实战将深入讲解如何结合这两个强大的技术栈,实现一个全栈应用。下面,我们将详细探讨Spring Boot后端框架与Vue3前端框架的集成应用。
花纵酒
2916
Spring Boot + Vue全栈开发实战随书源码附件
综上所述,"Spring Boot + Vue全栈开发实战"项目涉及的技术点包括Spring Boot的自动配置、Web开发、数据库操作、安全框架,以及Vue.js的组件化开发、状态管理、HTTP请求等
风起风来
819
Spring Boot Vue前后端分离开发实战.pdf
综上所述,文档《Spring Boot Vue前后端分离开发实战.pdf》涵盖了前后端分离的开发模式、Spring BootVue.js的基础知识和使用、开发环境的搭建、前后端交互的实现、项目配置启动
亚历山大伟
6725
3207257-Spring Boot+Vue全栈开发实战.pdf
《3207257-Spring Boot+Vue全栈开发实战.pdf》这份文档便是一本专注于讲解如何使用Spring BootVue.js进行全栈开发实战教程。
慕容峻才
476
# Spring Boot + Vue 全栈开发实战
该栏目下的文章为学习 王 松的《Spring Boot + Vue全栈开发实战》而做的笔记
Vue Spring Boot前后端分离开发实战》源码Vue+Spring Boot前后端分离开发实战教学课件(PPT)
这本《Vue Spring Boot前后端分离开发实战》的源码提供了深入学习和实践这一技术栈的机会。以下是对其中涉及知识点的详细说明:1.
1804
Spring Boot后端 + Vue管理员前端 + 微信小程序用户前端 + Vue用户移动端
**学习进阶**: 作为一名刚入行的开发者,你可以通过这个项目学习到如何集成多种技术,理解全栈开发流程,同时提升Spring Boot后端开发Vue.js前端以及微信小程序的实战技能。
热心网友!
2777
Spring Boot + Vue 全栈开发实战》官方案例.zip
本文档提供《Spring Boot + Vue 全栈开发实战》一书的官方示例代码,涵盖多个Spring Boot基础项目配置,展示Web服务搭建、环境管理及属性配置等核心功能,适合前后端分离架构的学习
JJJ69
6