一、项目介绍
体育场馆预约系统是一个基于Java开发的Web应用程序,旨在为用户提供便捷的场馆预约服务。系统主要面向学校、体育中心等场所,实现场馆资源的高效管理和使用。
二、系统功能模块
1. 用户管理模块
- 用户注册与登录
- 个人信息管理
- 角色权限控制(管理员/普通用户)
2. 场馆管理模块
- 场馆信息维护
- 场地状态管理
- 场馆类型分类
3. 预约管理模块
- 在线预约
- 预约记录查询
- 预约取消功能
- 预约时段冲突检测
4. 公告管理模块
- 系统公告发布
- 场馆使用规则
- 预约须知
三、技术架构
后端技术栈:
- Spring Boot
- Spring Security
- MyBatis Plus
- MySQL
- Redis
前端技术栈:
- Vue.js
- Element UI
- Axios
- Vue Router
四、数据库设计
主要数据表
-- 用户表
CREATE TABLE user (
id INT PRIMARY KEY AUTO_INCREMENT,
username VARCHAR(50),
password VARCHAR(100),
role VARCHAR(20),
create_time DATETIME
);
-- 场馆表
CREATE TABLE venue (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(100),
type VARCHAR(50),
status INT,
description TEXT
);
-- 预约记录表
CREATE TABLE booking (
id INT PRIMARY KEY AUTO_INCREMENT,
user_id INT,
venue_id INT,
booking_date DATE,
time_slot VARCHAR(50),
status INT
);
五、核心功能实现
1. 预约功能实现
@Service
public class BookingService {
@Autowired
private BookingMapper bookingMapper;
public Result bookVenue(BookingDTO bookingDTO) {
// 检查时段是否可用
if (!checkTimeSlotAvailable(bookingDTO)) {
return Result.error("该时段已被预约");
}
// 创建预约记录
Booking booking = new Booking();
booking.setUserId(bookingDTO.getUserId());
booking.setVenueId(bookingDTO.getVenueId());
booking.setBookingDate(bookingDTO.getBookingDate());
booking.setTimeSlot(bookingDTO.getTimeSlot());
booking.setStatus(BookingStatus.CONFIRMED.getCode());
bookingMapper.insert(booking);
return Result.success("预约成功");
}
}
2. 场馆查询功能
@RestController
@RequestMapping("/venue")
public class VenueController {
@Autowired
private VenueService venueService;
@GetMapping("/list")
public Result getVenueList(VenueQueryDTO queryDTO) {
Page<Venue> page = venueService.queryVenueList(queryDTO);
return Result.success(page);
}
}
六、预约管理模块详细设计
1、数据库设计
-- 预约记录表
CREATE TABLE booking (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
user_id BIGINT NOT NULL COMMENT '用户ID',
venue_id BIGINT NOT NULL COMMENT '场馆ID',
booking_date DATE NOT NULL COMMENT '预约日期',
time_slot VARCHAR(20) NOT NULL COMMENT '预约时段',
status TINYINT NOT NULL COMMENT '状态:0-待确认,1-已确认,2-已取消,3-已完成',
create_time DATETIME NOT NULL COMMENT '创建时间',
update_time DATETIME NOT NULL COMMENT '更新时间',
remark VARCHAR(200) COMMENT '备注',
UNIQUE KEY uk_venue_date_slot (venue_id, booking_date, time_slot)
);
2、实体类设计
@Data
public class Booking {
private Long id;
private Long userId;
private Long venueId;
private LocalDate bookingDate;
private String timeSlot;
private Integer status;
private LocalDateTime createTime;
private LocalDateTime updateTime;
private String remark;
}
@Data
public class BookingDTO {
@NotNull(message = "场馆ID不能为空")
private Long venueId;
@NotNull(message = "预约日期不能为空")
@Future(message = "预约日期必须是将来日期")
private LocalDate bookingDate;
@NotEmpty(message = "预约时段不能为空")
private String timeSlot;
private String remark;
}
3、Service层实现
@Service
@Slf4j
public class BookingService {
@Autowired
private BookingMapper bookingMapper;
@Autowired
private RedisTemplate redisTemplate;
/**
* 创建预约
*/
@Transactional
public Result createBooking(BookingDTO dto, Long userId) {
// 1. 检查预约时间是否合法
if (!isValidBookingTime(dto.getBookingDate(), dto.getTimeSlot())) {
return Result.error("预约时间不在允许范围内");
}
// 2. 检查时段冲突(使用Redis实现分布式锁)
String lockKey = String.format("booking:lock:%d:%s:%s",
dto.getVenueId(), dto.getBookingDate(), dto.getTimeSlot());
boolean locked = redisTemplate.opsForValue()
.setIfAbsent(lockKey, "1", 10, TimeUnit.SECONDS);
if (!locked) {
return Result.error("该时段正在被其他用户预约,请稍后重试");
}
try {
// 3. 检查时段是否已被预约
if (checkTimeSlotConflict(dto)) {
return Result.error("该时段已被预约");
}
// 4. 创建预约记录
Booking booking = new Booking();
BeanUtils.copyProperties(dto, booking);
booking.setUserId(userId);
booking.setStatus(BookingStatus.PENDING.getCode());
booking.setCreateTime(LocalDateTime.now());
booking.setUpdateTime(LocalDateTime.now());
bookingMapper.insert(booking);
return Result.success("预约成功");
} finally {
redisTemplate.delete(lockKey);
}
}
/**
* 取消预约
*/
@Transactional
public Result cancelBooking(Long bookingId, Long userId) {
Booking booking = bookingMapper.selectById(bookingId);
if (booking == null) {
return Result.error("预约记录不存在");
}
if (!booking.getUserId().equals(userId)) {
return Result.error("无权操作此预约");
}
if (!BookingStatus.canCancel(booking.getStatus())) {
return Result.error("当前状态不允许取消");
}
booking.setStatus(BookingStatus.CANCELLED.getCode());
booking.setUpdateTime(LocalDateTime.now());
bookingMapper.updateById(booking);
return Result.success("取消成功");
}
/**
* 查询预约记录
*/
public PageResult<BookingVO> queryBookings(BookingQueryDTO queryDTO) {
Page<Booking> page = new Page<>(queryDTO.getPageNum(), queryDTO.getPageSize());
LambdaQueryWrapper<Booking> wrapper = new LambdaQueryWrapper<Booking>()
.eq(queryDTO.getUserId() != null, Booking::getUserId, queryDTO.getUserId())
.eq(queryDTO.getVenueId() != null, Booking::getVenueId, queryDTO.getVenueId())
.ge(queryDTO.getStartDate() != null, Booking::getBookingDate, queryDTO.getStartDate())
.le(queryDTO.getEndDate() != null, Booking::getBookingDate, queryDTO.getEndDate())
.eq(queryDTO.getStatus() != null, Booking::getStatus, queryDTO.getStatus())
.orderByDesc(Booking::getCreateTime);
Page<Booking> resultPage = bookingMapper.selectPage(page, wrapper);
// 转换为VO
List<BookingVO> bookingVOs = resultPage.getRecords().stream()
.map(this::convertToVO)
.collect(Collectors.toList());
return new PageResult<>(bookingVOs, resultPage.getTotal());
}
/**
* 检查时段冲突
*/
private boolean checkTimeSlotConflict(BookingDTO dto) {
LambdaQueryWrapper<Booking> wrapper = new LambdaQueryWrapper<Booking>()
.eq(Booking::getVenueId, dto.getVenueId())
.eq(Booking::getBookingDate, dto.getBookingDate())
.eq(Booking::getTimeSlot, dto.getTimeSlot())
.ne(Booking::getStatus, BookingStatus.CANCELLED.getCode());
return bookingMapper.selectCount(wrapper) > 0;
}
/**
* 验证预约时间是否合法
*/
private boolean isValidBookingTime(LocalDate bookingDate, String timeSlot) {
// 1. 检查是否是将来日期
if (bookingDate.isBefore(LocalDate.now())) {
return false;
}
// 2. 检查是否在可预约天数范围内(如:最多预约30天后的场地)
if (bookingDate.isAfter(LocalDate.now().plusDays(30))) {
return false;
}
// 3. 检查时段格式是否正确(如:10:00-11:00)
if (!timeSlot.matches("\\d{2}:\\d{2}-\\d{2}:\\d{2}")) {
return false;
}
// 4. 检查是否在营业时间内
String[] times = timeSlot.split("-");
LocalTime startTime = LocalTime.parse(times[0]);
LocalTime endTime = LocalTime.parse(times[1]);
return startTime.isAfter(LocalTime.of(8, 0)) &&
endTime.isBefore(LocalTime.of(22, 0));
}
}
4、Controller层实现
@RestController
@RequestMapping("/api/bookings")
@Validated
public class BookingController {
@Autowired
private BookingService bookingService;
/**
* 创建预约
*/
@PostMapping
public Result createBooking(@RequestBody @Valid BookingDTO dto) {
Long userId = SecurityUtils.getCurrentUserId();
return bookingService.createBooking(dto, userId);
}
/**
* 取消预约
*/
@PostMapping("/{id}/cancel")
public Result cancelBooking(@PathVariable("id") Long bookingId) {
Long userId = SecurityUtils.getCurrentUserId();
return bookingService.cancelBooking(bookingId, userId);
}
/**
* 查询预约记录
*/
@GetMapping
public Result<PageResult<BookingVO>> queryBookings(BookingQueryDTO queryDTO) {
Long userId = SecurityUtils.getCurrentUserId();
// 普通用户只能查询自己的预约记录
if (!SecurityUtils.isAdmin()) {
queryDTO.setUserId(userId);
}
return Result.success(bookingService.queryBookings(queryDTO));
}
}
5、前端实现(Vue)
<template>
<div class="booking-container">
<!-- 预约表单 -->
<el-form :model="bookingForm" :rules="rules" ref="bookingForm">
<el-form-item label="场馆" prop="venueId">
<el-select v-model="bookingForm.venueId">
<el-option
v-for="venue in venues"
:key="venue.id"
:label="venue.name"
:value="venue.id"
/>
</el-select>
</el-form-item>
<el-form-item label="日期" prop="bookingDate">
<el-date-picker
v-model="bookingForm.bookingDate"
type="date"
:picker-options="dateOptions"
/>
</el-form-item>
<el-form-item label="时段" prop="timeSlot">
<el-select v-model="bookingForm.timeSlot">
<el-option
v-for="slot in timeSlots"
:key="slot.value"
:label="slot.label"
:value="slot.value"
/>
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="submitBooking">
提交预约
</el-button>
</el-form-item>
</el-form>
<!-- 预约记录列表 -->
<el-table :data="bookings" border>
<el-table-column prop="venueName" label="场馆" />
<el-table-column prop="bookingDate" label="日期" />
<el-table-column prop="timeSlot" label="时段" />
<el-table-column prop="status" label="状态">
<template slot-scope="scope">
<el-tag :type="getStatusType(scope.row.status)">
{{ getStatusText(scope.row.status) }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作">
<template slot-scope="scope">
<el-button
v-if="canCancel(scope.row)"
type="danger"
size="small"
@click="cancelBooking(scope.row.id)"
>
取消预约
</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination
:current-page.sync="query.pageNum"
:page-size="query.pageSize"
:total="total"
@current-change="handlePageChange"
/>
</div>
</template>
<script>
export default {
data() {
return {
bookingForm: {
venueId: '',
bookingDate: '',
timeSlot: ''
},
rules: {
venueId: [{ required: true, message: '请选择场馆' }],
bookingDate: [{ required: true, message: '请选择日期' }],
timeSlot: [{ required: true, message: '请选择时段' }]
},
venues: [],
bookings: [],
query: {
pageNum: 1,
pageSize: 10
},
total: 0,
dateOptions: {
disabledDate(time) {
return time.getTime() < Date.now() - 8.64e7;
}
}
};
},
created() {
this.loadVenues();
this.loadBookings();
},
methods: {
async submitBooking() {
try {
await this.$refs.bookingForm.validate();
const res = await this.$api.post('/bookings', this.bookingForm);
this.$message.success('预约成功');
this.loadBookings();
} catch (error) {
this.$message.error(error.message);
}
},
async cancelBooking(id) {
try {
await this.$confirm('确认取消该预约?');
await this.$api.post(`/bookings/${id}/cancel`);
this.$message.success('取消成功');
this.loadBookings();
} catch (error) {
if (error !== 'cancel') {
this.$message.error(error.message);
}
}
},
async loadBookings() {
const res = await this.$api.get('/bookings', { params: this.query });
this.bookings = res.data.records;
this.total = res.data.total;
},
handlePageChange() {
this.loadBookings();
},
canCancel(booking) {
return booking.status === 0 || booking.status === 1;
}
}
};
</script>
6、关键点说明
-
并发控制
- 使用Redis分布式锁防止并发预约
- 数据库唯一索引确保数据一致性
-
预约规则
- 预约时间范围限制
- 营业时间验证
- 状态流转控制
-
性能优化
- 分页查询
- 索引优化
- 缓存使用
-
安全控制
- 身份验证
- 权限校验
- 数据隔离
-
用户体验
- 友好的错误提示
- 实时状态更新
- 便捷的操作界面
通过以上设计和实现,可以为用户提供一个功能完善、体验良好的预约管理模块。
七、系统特点
-
实时性
- 采用Redis缓存热点数据
- 实时更新场馆预约状态
-
可扩展性
- 模块化设计
- 支持新增场馆类型
- 灵活的预约规则配置
-
用户友好
- 直观的预约界面
- 便捷的查询功能
- 完善的提示信息
八、系统优化
-
性能优化
- 数据库索引优化
- 接口响应时间优化
- 并发预约处理
-
安全性优化
- Spring Security安全框架
- 接口访问权限控制
- 数据加密传输
九、总结与展望
本系统实现了体育场馆预约的基本功能,为用户提供了便捷的预约服务。未来可以考虑添加以下功能:
- 在线支付功能
- 场馆评价系统
- 微信小程序端
- 智能推荐功能
- 数据统计分析
通过不断优化和完善,使系统更好地服务于用户,提高场馆资源利用率。