作为一名经历过无数次图书馆抢座大战的程序员,我深知一个稳定可靠的预约系统对学生有多重要。今天要分享的这套基于SSM框架的图书馆预约选座系统,是我带领团队经过三个版本迭代后的实战成果。系统日均处理3000+预约请求,在期末考试周高峰期稳定运行无压力。
系统采用经典的SSM(Spring+SpringMVC+MyBatis)架构组合,配合MySQL数据库,实现了包括座位预约、公告管理、用户权限控制等核心功能。前端使用Bootstrap框架保证响应式布局,特别针对移动端操作做了深度优化。整个项目代码结构清晰,采用Maven进行依赖管理,非常适合作为Java学习者的进阶练手项目。
技术选型说明:之所以选择SSM而非Spring Boot,主要是考虑到教学场景下需要更清晰地展示各层配置关系。实际企业级项目可以无缝升级到Spring Boot体系。
系统严格遵循MVC设计模式,各层职责分明:
java复制// 典型Controller示例
@Controller
@RequestMapping("/seat")
public class SeatController {
@Autowired
private SeatService seatService;
@GetMapping("/list")
@ResponseBody
public Result listSeats(@RequestParam(defaultValue="1") Integer pageNum) {
PageHelper.startPage(pageNum, 10);
return Result.success(seatService.listAvailableSeats());
}
}
数据库表设计遵循第三范式,核心表包括:
sql复制CREATE TABLE `seat` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`floor` varchar(10) NOT NULL COMMENT '楼层',
`area` varchar(20) NOT NULL COMMENT '区域',
`number` varchar(10) NOT NULL COMMENT '座位编号',
`status` tinyint(4) DEFAULT '0' COMMENT '0-空闲 1-已预约',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `idx_location` (`floor`,`area`,`number`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
系统采用基于注解的权限控制方案,相比传统配置文件方式更加灵活:
java复制// 自定义权限注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RequireRole {
int[] value() default {};
}
// 在拦截器中校验权限
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
Method method = ((HandlerMethod) handler).getMethod();
if (method.isAnnotationPresent(RequireRole.class)) {
int[] allowedRoles = method.getAnnotation(RequireRole.class).value();
User user = (User) request.getSession().getAttribute("user");
if (!Arrays.stream(allowedRoles).anyMatch(r -> r == user.getRole())) {
throw new UnauthorizedException("权限不足");
}
}
return true;
}
预约功能的核心在于处理并发冲突,我们采用乐观锁+事务的方案:
java复制@Transactional(rollbackFor = Exception.class)
public Result reserveSeat(ReserveDTO dto) {
// 检查时间冲突
if (reservationMapper.countConflict(dto.getSeatId(),
dto.getStartTime(), dto.getEndTime()) > 0) {
throw new BusinessException("该时段已被预约");
}
// 更新座位状态
Seat seat = seatMapper.selectForUpdate(dto.getSeatId());
if (seat.getStatus() != SeatStatus.AVAILABLE) {
throw new BusinessException("座位不可用");
}
seat.setStatus(SeatStatus.RESERVED);
seatMapper.updateByPrimaryKey(seat);
// 创建预约记录
Reservation reservation = new Reservation();
BeanUtils.copyProperties(dto, reservation);
reservation.setUserId(SessionUtils.getUserId());
reservationMapper.insert(reservation);
return Result.success(reservation.getId());
}
关键点说明:selectForUpdate使用行锁确保并发安全,@Transactional保证操作原子性
采用WebSocket实现座位状态实时更新,避免频繁轮询:
java复制@ServerEndpoint("/seatStatus")
@Component
public class SeatStatusEndpoint {
private static final Map<String, Session> sessions = new ConcurrentHashMap<>();
@OnOpen
public void onOpen(Session session) {
sessions.put(session.getId(), session);
}
@OnClose
public void onClose(Session session) {
sessions.remove(session.getId());
}
public static void broadcastSeatUpdate(Seat seat) {
String message = JSON.toJSONString(seat);
sessions.values().forEach(session -> {
try {
session.getBasicRemote().sendText(message);
} catch (IOException e) {
log.error("推送消息失败", e);
}
});
}
}
前端通过事件驱动更新UI:
javascript复制const socket = new WebSocket(`ws://${location.host}/seatStatus`);
socket.onmessage = (event) => {
const seat = JSON.parse(event.data);
const element = document.querySelector(`.seat-${seat.id}`);
element.classList.toggle('occupied', seat.status === 1);
};
使用Spring的@Scheduled注解实现自动释放超时未签到的预约:
java复制@Scheduled(cron = "0 0/30 * * * ?")
public void releaseExpiredReservations() {
List<Reservation> expired = reservationMapper.selectExpired(LocalDateTime.now());
expired.forEach(reservation -> {
seatMapper.updateStatus(reservation.getSeatId(), SeatStatus.AVAILABLE);
reservationMapper.updateStatus(reservation.getId(), ReservationStatus.EXPIRED);
log.info("释放过期预约:{}", reservation.getId());
});
}
xml复制<!-- MyBatis批量插入示例 -->
<insert id="batchInsert" parameterType="java.util.List">
INSERT INTO student (name, number, class_id) VALUES
<foreach collection="list" item="item" separator=",">
(#{item.name}, #{item.number}, #{item.classId})
</foreach>
</insert>
java复制// 乐观锁实现示例
public boolean updateWithLock(Seat seat) {
seat.setVersion(seat.getVersion() + 1);
return seatMapper.updateWithVersion(seat) > 0;
}
mvn clean packageproperties复制# 数据库配置示例
spring.datasource.url=jdbc:mysql://localhost:3306/library?useSSL=false
spring.datasource.username=root
spring.datasource.password=123456
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
java复制// 分布式锁示例
public boolean tryLock(String key, long expireSeconds) {
return redisTemplate.opsForValue()
.setIfAbsent(key, "1", expireSeconds, TimeUnit.SECONDS);
}
这个项目从第一行代码到最终上线,我们踩过不少坑也积累了很多实战经验。特别是在高并发场景下的处理方案,经过多次优化现在已经能够稳定支持校园级的预约需求。源码中还有很多值得研究的细节,比如AOP日志、参数校验、异常处理等,都是企业级开发中的必备技能。