这个基于Vue.js和Node.js的旅游网站平台是一个典型的B/S架构应用,采用了前后端分离的开发模式。前端使用Vue.js框架构建用户界面,后端则基于Node.js和Express框架实现业务逻辑处理,数据存储采用MySQL关系型数据库。平台主要面向两类用户:普通游客和系统管理员,提供了完整的旅游信息查询、预订、社区交流等功能。
作为一个全栈项目,它涵盖了从用户界面到数据库设计的完整开发流程。前端负责展示数据和收集用户输入,后端处理业务逻辑并与数据库交互,两者通过RESTful API进行通信。这种架构设计使得系统具有良好的可维护性和扩展性,能够适应未来业务需求的增长。
前端采用Vue.js作为主要框架,这是一个渐进式JavaScript框架,具有以下优势:
在实际开发中,我们使用Vue CLI快速搭建项目结构,通过Webpack进行模块打包。对于UI组件,可以选择Element UI或Ant Design Vue等成熟的UI库,它们提供了丰富的预设组件,可以显著加快开发速度。
后端基于Node.js和Express框架构建,主要特点包括:
数据库选用MySQL,这是一个成熟的关系型数据库管理系统,适合存储结构化数据。我们使用Sequelize作为ORM工具,它提供了以下功能:
系统采用典型的三层架构:
这种分层架构使得各层职责明确,便于团队协作和后期维护。前后端完全分离,通过API接口通信,使得前端可以独立于后端进行开发和测试。
用户认证是系统的安全基础,我们采用JWT(JSON Web Token)实现认证流程:
javascript复制// 用户注册API示例
router.post('/register', async (req, res) => {
try {
const { username, password, email } = req.body;
// 检查用户名是否已存在
const existingUser = await User.findOne({ where: { username } });
if (existingUser) {
return res.status(400).json({ message: '用户名已存在' });
}
// 密码加密存储
const hashedPassword = await bcrypt.hash(password, 10);
// 创建新用户
const newUser = await User.create({
username,
password: hashedPassword,
email,
role: 'user'
});
res.status(201).json({ message: '注册成功' });
} catch (error) {
res.status(500).json({ message: '服务器错误' });
}
});
javascript复制// 用户登录API示例
router.post('/login', async (req, res) => {
try {
const { username, password } = req.body;
// 查找用户
const user = await User.findOne({ where: { username } });
if (!user) {
return res.status(401).json({ message: '用户名或密码错误' });
}
// 验证密码
const isMatch = await bcrypt.compare(password, user.password);
if (!isMatch) {
return res.status(401).json({ message: '用户名或密码错误' });
}
// 生成JWT令牌
const token = jwt.sign(
{ userId: user.id, role: user.role },
process.env.JWT_SECRET,
{ expiresIn: '2h' }
);
res.json({ token, userId: user.id });
} catch (error) {
res.status(500).json({ message: '服务器错误' });
}
});
javascript复制// JWT认证中间件
const authMiddleware = (req, res, next) => {
const token = req.headers.authorization?.split(' ')[1];
if (!token) {
return res.status(401).json({ message: '未提供认证令牌' });
}
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
req.user = decoded;
next();
} catch (error) {
res.status(401).json({ message: '无效的认证令牌' });
}
};
景点管理模块包括景点信息的增删改查功能,核心实现如下:
javascript复制// 景点模型定义
const Attraction = sequelize.define('Attraction', {
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true
},
name: {
type: DataTypes.STRING(64),
allowNull: false
},
description: {
type: DataTypes.TEXT,
allowNull: false
},
price: {
type: DataTypes.DECIMAL(10, 2),
allowNull: false
},
location: {
type: DataTypes.STRING(64),
allowNull: false
},
openingHours: {
type: DataTypes.STRING(64),
allowNull: false
},
images: {
type: DataTypes.TEXT,
allowNull: false,
get() {
const rawValue = this.getDataValue('images');
return rawValue ? JSON.parse(rawValue) : [];
},
set(value) {
this.setDataValue('images', JSON.stringify(value));
}
}
}, {
tableName: 'attractions',
timestamps: true
});
javascript复制// 获取景点列表API
router.get('/attractions', async (req, res) => {
try {
const { page = 1, limit = 10, keyword } = req.query;
const offset = (page - 1) * limit;
const where = {};
if (keyword) {
where.name = { [Op.like]: `%${keyword}%` };
}
const { count, rows } = await Attraction.findAndCountAll({
where,
offset,
limit: parseInt(limit),
order: [['createdAt', 'DESC']]
});
res.json({
total: count,
page: parseInt(page),
limit: parseInt(limit),
data: rows
});
} catch (error) {
res.status(500).json({ message: '服务器错误' });
}
});
javascript复制// 获取景点详情API
router.get('/attractions/:id', async (req, res) => {
try {
const attraction = await Attraction.findByPk(req.params.id);
if (!attraction) {
return res.status(404).json({ message: '景点不存在' });
}
// 增加点击量
await attraction.increment('hits');
res.json(attraction);
} catch (error) {
res.status(500).json({ message: '服务器错误' });
}
});
酒店预订是平台的核心功能之一,实现流程如下:
javascript复制// 酒店模型定义
const Hotel = sequelize.define('Hotel', {
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true
},
name: {
type: DataTypes.STRING(64),
allowNull: false
},
address: {
type: DataTypes.STRING(255),
allowNull: false
},
phone: {
type: DataTypes.STRING(20),
allowNull: false
},
description: {
type: DataTypes.TEXT,
allowNull: false
},
images: {
type: DataTypes.TEXT,
allowNull: false,
get() {
const rawValue = this.getDataValue('images');
return rawValue ? JSON.parse(rawValue) : [];
},
set(value) {
this.setDataValue('images', JSON.stringify(value));
}
}
}, {
tableName: 'hotels',
timestamps: true
});
// 房间类型模型
const RoomType = sequelize.define('RoomType', {
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true
},
name: {
type: DataTypes.STRING(64),
allowNull: false
},
price: {
type: DataTypes.DECIMAL(10, 2),
allowNull: false
},
description: {
type: DataTypes.TEXT,
allowNull: false
},
maxOccupancy: {
type: DataTypes.INTEGER,
allowNull: false
}
}, {
tableName: 'room_types',
timestamps: true
});
// 定义关联关系
Hotel.hasMany(RoomType, { foreignKey: 'hotelId' });
RoomType.belongsTo(Hotel, { foreignKey: 'hotelId' });
javascript复制// 创建酒店预订API
router.post('/bookings', authMiddleware, async (req, res) => {
try {
const { roomTypeId, checkInDate, checkOutDate, guestCount, specialRequests } = req.body;
const userId = req.user.userId;
// 验证房间类型是否存在
const roomType = await RoomType.findByPk(roomTypeId);
if (!roomType) {
return res.status(404).json({ message: '房间类型不存在' });
}
// 检查房间可用性
const existingBookings = await Booking.count({
where: {
roomTypeId,
checkInDate: { [Op.lt]: checkOutDate },
checkOutDate: { [Op.gt]: checkInDate },
status: { [Op.notIn]: ['cancelled', 'completed'] }
}
});
const availableRooms = await Room.count({
where: { roomTypeId }
});
if (existingBookings >= availableRooms) {
return res.status(400).json({ message: '该日期范围内没有可用房间' });
}
// 计算总价
const nights = Math.ceil((new Date(checkOutDate) - new Date(checkInDate)) / (1000 * 60 * 60 * 24));
const totalPrice = roomType.price * nights;
// 创建预订
const booking = await Booking.create({
userId,
roomTypeId,
hotelId: roomType.hotelId,
checkInDate,
checkOutDate,
guestCount,
specialRequests,
totalPrice,
status: 'confirmed'
});
res.status(201).json(booking);
} catch (error) {
res.status(500).json({ message: '服务器错误' });
}
});
系统采用关系型数据库设计,主要表及其关系如下:
关键表关系:
为提高查询性能,我们在以下字段上创建了索引:
用户表:
景点表:
预订表:
为确保数据一致性,我们采取了以下措施:
例如,创建预订时的事务处理:
javascript复制// 使用事务处理预订创建
const createBookingWithTransaction = async (bookingData) => {
const transaction = await sequelize.transaction();
try {
// 检查房间可用性
const available = await checkRoomAvailability(
bookingData.roomTypeId,
bookingData.checkInDate,
bookingData.checkOutDate,
{ transaction }
);
if (!available) {
throw new Error('房间不可用');
}
// 创建预订
const booking = await Booking.create(bookingData, { transaction });
// 更新房间状态
await updateRoomStatus(bookingData.roomTypeId, 'reserved', { transaction });
// 提交事务
await transaction.commit();
return booking;
} catch (error) {
// 回滚事务
await transaction.rollback();
throw error;
}
};
前端采用模块化组件设计,主要组件结构如下:
布局组件:
页面组件:
通用组件:
使用Vuex管理应用状态,主要store模块包括:
示例auth模块实现:
javascript复制// store/modules/auth.js
const state = {
user: null,
token: null,
isAuthenticated: false
};
const mutations = {
SET_USER(state, { user, token }) {
state.user = user;
state.token = token;
state.isAuthenticated = true;
// 将token存储到localStorage
localStorage.setItem('token', token);
},
CLEAR_USER(state) {
state.user = null;
state.token = null;
state.isAuthenticated = false;
localStorage.removeItem('token');
}
};
const actions = {
async login({ commit }, credentials) {
try {
const response = await api.post('/auth/login', credentials);
commit('SET_USER', {
user: response.data.user,
token: response.data.token
});
return response;
} catch (error) {
throw error;
}
},
logout({ commit }) {
commit('CLEAR_USER');
},
async register(_, userData) {
try {
const response = await api.post('/auth/register', userData);
return response;
} catch (error) {
throw error;
}
}
};
const getters = {
currentUser: state => state.user,
isAuthenticated: state => state.isAuthenticated
};
export default {
state,
mutations,
actions,
getters
};
使用Vue Router管理前端路由,主要配置如下:
javascript复制// router/index.js
import Vue from 'vue';
import VueRouter from 'vue-router';
import store from '@/store';
import Home from '@/views/Home.vue';
import Attractions from '@/views/Attractions.vue';
import AttractionDetail from '@/views/AttractionDetail.vue';
import Hotels from '@/views/Hotels.vue';
import HotelDetail from '@/views/HotelDetail.vue';
import Booking from '@/views/Booking.vue';
import UserProfile from '@/views/UserProfile.vue';
import Login from '@/views/Login.vue';
import Register from '@/views/Register.vue';
Vue.use(VueRouter);
const routes = [
{
path: '/',
name: 'Home',
component: Home
},
{
path: '/attractions',
name: 'Attractions',
component: Attractions
},
{
path: '/attractions/:id',
name: 'AttractionDetail',
component: AttractionDetail,
props: true
},
{
path: '/hotels',
name: 'Hotels',
component: Hotels
},
{
path: '/hotels/:id',
name: 'HotelDetail',
component: HotelDetail,
props: true
},
{
path: '/book/:roomTypeId',
name: 'Booking',
component: Booking,
props: true,
meta: { requiresAuth: true }
},
{
path: '/profile',
name: 'UserProfile',
component: UserProfile,
meta: { requiresAuth: true }
},
{
path: '/login',
name: 'Login',
component: Login,
meta: { guestOnly: true }
},
{
path: '/register',
name: 'Register',
component: Register,
meta: { guestOnly: true }
}
];
const router = new VueRouter({
mode: 'history',
base: process.env.BASE_URL,
routes
});
// 全局路由守卫
router.beforeEach((to, from, next) => {
const isAuthenticated = store.getters['auth/isAuthenticated'];
// 检查是否需要认证
if (to.matched.some(record => record.meta.requiresAuth) && !isAuthenticated) {
next({ name: 'Login', query: { redirect: to.fullPath } });
return;
}
// 检查是否仅允许未登录用户访问
if (to.matched.some(record => record.meta.guestOnly) && isAuthenticated) {
next({ name: 'Home' });
return;
}
next();
});
export default router;
系统采用Docker容器化部署,主要包含以下服务:
Docker-compose配置示例:
yaml复制version: '3.8'
services:
frontend:
image: nginx:alpine
ports:
- "80:80"
volumes:
- ./frontend/dist:/usr/share/nginx/html
- ./nginx.conf:/etc/nginx/conf.d/default.conf
depends_on:
- backend
restart: always
backend:
build:
context: ./backend
dockerfile: Dockerfile
ports:
- "3000:3000"
environment:
- NODE_ENV=production
- DB_HOST=db
- DB_USER=root
- DB_PASSWORD=secret
- DB_NAME=travel_platform
- JWT_SECRET=your_jwt_secret
depends_on:
- db
restart: always
db:
image: mysql:8.0
ports:
- "3306:3306"
environment:
- MYSQL_ROOT_PASSWORD=secret
- MYSQL_DATABASE=travel_platform
volumes:
- mysql_data:/var/lib/mysql
restart: always
volumes:
mysql_data:
为提高系统性能,我们实施了以下优化措施:
前端优化:
后端优化:
数据库优化:
系统安全防护措施包括:
认证安全:
输入验证:
API安全:
数据保护:
系统采用分层测试策略,确保各组件质量:
单元测试:
E2E测试:
javascript复制// 用户登录测试
describe('POST /auth/login', () => {
it('should authenticate valid user', async () => {
const res = await request(app)
.post('/auth/login')
.send({
username: 'testuser',
password: 'password123'
});
expect(res.statusCode).toEqual(200);
expect(res.body).toHaveProperty('token');
expect(res.body).toHaveProperty('userId');
});
it('should reject invalid credentials', async () => {
const res = await request(app)
.post('/auth/login')
.send({
username: 'wronguser',
password: 'wrongpassword'
});
expect(res.statusCode).toEqual(401);
expect(res.body).toHaveProperty('message');
});
});
javascript复制// 景点列表API测试
describe('GET /attractions', () => {
before(async () => {
// 插入测试数据
await Attraction.bulkCreate([
{ name: 'Test Attraction 1', description: 'Desc 1', price: 100, location: 'Test', openingHours: '9-18' },
{ name: 'Test Attraction 2', description: 'Desc 2', price: 200, location: 'Test', openingHours: '9-18' }
]);
});
it('should return paginated attraction list', async () => {
const res = await request(app)
.get('/attractions?page=1&limit=10');
expect(res.statusCode).toEqual(200);
expect(res.body).toHaveProperty('data');
expect(res.body.data.length).toBeGreaterThan(0);
});
});
javascript复制// AttractionCard组件测试
import { shallowMount } from '@vue/test-utils';
import AttractionCard from '@/components/AttractionCard.vue';
describe('AttractionCard.vue', () => {
it('renders attraction data correctly', () => {
const attraction = {
id: 1,
name: 'Test Attraction',
description: 'Test Description',
price: 100,
location: 'Test Location'
};
const wrapper = shallowMount(AttractionCard, {
propsData: { attraction }
});
expect(wrapper.find('.card-title').text()).toBe(attraction.name);
expect(wrapper.find('.card-text').text()).toContain(attraction.description);
expect(wrapper.find('.price').text()).toContain(attraction.price.toString());
});
});
通过这个项目的开发,我们获得了以下技术经验:
性能瓶颈:
数据一致性:
用户体验:
安全性:
功能扩展:
技术升级:
性能优化:
监控运维: